window-basic-status-bar.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. #include <QPainter>
  2. #include <QPixmap>
  3. #include "obs-app.hpp"
  4. #include "window-basic-main.hpp"
  5. #include "window-basic-status-bar.hpp"
  6. #include "window-basic-main-outputs.hpp"
  7. #include "qt-wrappers.hpp"
  8. #include "platform.hpp"
  9. #include "ui_StatusBarWidget.h"
  10. static constexpr int bitrateUpdateSeconds = 2;
  11. static constexpr int congestionUpdateSeconds = 4;
  12. static constexpr float excellentThreshold = 0.0f;
  13. static constexpr float goodThreshold = 0.3333f;
  14. static constexpr float mediocreThreshold = 0.6667f;
  15. static constexpr float badThreshold = 1.0f;
  16. StatusBarWidget::StatusBarWidget(QWidget *parent)
  17. : QWidget(parent),
  18. ui(new Ui::StatusBarWidget)
  19. {
  20. ui->setupUi(this);
  21. }
  22. StatusBarWidget::~StatusBarWidget() {}
  23. OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent)
  24. : QStatusBar(parent),
  25. excellentPixmap(QIcon(":/res/images/network-excellent.svg")
  26. .pixmap(QSize(16, 16))),
  27. goodPixmap(
  28. QIcon(":/res/images/network-good.svg").pixmap(QSize(16, 16))),
  29. mediocrePixmap(QIcon(":/res/images/network-mediocre.svg")
  30. .pixmap(QSize(16, 16))),
  31. badPixmap(
  32. QIcon(":/res/images/network-bad.svg").pixmap(QSize(16, 16))),
  33. recordingActivePixmap(QIcon(":/res/images/recording-active.svg")
  34. .pixmap(QSize(16, 16))),
  35. recordingPausePixmap(QIcon(":/res/images/recording-pause.svg")
  36. .pixmap(QSize(16, 16))),
  37. streamingActivePixmap(QIcon(":/res/images/streaming-active.svg")
  38. .pixmap(QSize(16, 16)))
  39. {
  40. statusWidget = new StatusBarWidget(this);
  41. statusWidget->ui->delayInfo->setText("");
  42. statusWidget->ui->droppedFrames->setText(
  43. QTStr("DroppedFrames").arg("0", "0.0"));
  44. statusWidget->ui->statusIcon->setPixmap(inactivePixmap);
  45. statusWidget->ui->streamIcon->setPixmap(streamingInactivePixmap);
  46. statusWidget->ui->streamTime->setDisabled(true);
  47. statusWidget->ui->recordIcon->setPixmap(recordingInactivePixmap);
  48. statusWidget->ui->recordTime->setDisabled(true);
  49. statusWidget->ui->delayFrame->hide();
  50. statusWidget->ui->issuesFrame->hide();
  51. statusWidget->ui->kbps->hide();
  52. addPermanentWidget(statusWidget);
  53. setMinimumHeight(statusWidget->height());
  54. UpdateIcons();
  55. connect(App(), &OBSApp::StyleChanged, this,
  56. &OBSBasicStatusBar::UpdateIcons);
  57. }
  58. void OBSBasicStatusBar::Activate()
  59. {
  60. if (!active) {
  61. refreshTimer = new QTimer(this);
  62. connect(refreshTimer, &QTimer::timeout, this,
  63. &OBSBasicStatusBar::UpdateStatusBar);
  64. int skipped = video_output_get_skipped_frames(obs_get_video());
  65. int total = video_output_get_total_frames(obs_get_video());
  66. totalStreamSeconds = 0;
  67. totalRecordSeconds = 0;
  68. lastSkippedFrameCount = 0;
  69. startSkippedFrameCount = skipped;
  70. startTotalFrameCount = total;
  71. refreshTimer->start(1000);
  72. active = true;
  73. if (streamOutput) {
  74. statusWidget->ui->statusIcon->setPixmap(inactivePixmap);
  75. }
  76. }
  77. if (streamOutput) {
  78. statusWidget->ui->streamIcon->setPixmap(streamingActivePixmap);
  79. statusWidget->ui->streamTime->setDisabled(false);
  80. statusWidget->ui->issuesFrame->show();
  81. statusWidget->ui->kbps->show();
  82. firstCongestionUpdate = true;
  83. }
  84. if (recordOutput) {
  85. statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap);
  86. statusWidget->ui->recordTime->setDisabled(false);
  87. }
  88. }
  89. void OBSBasicStatusBar::Deactivate()
  90. {
  91. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  92. if (!main)
  93. return;
  94. if (!streamOutput) {
  95. statusWidget->ui->streamTime->setText(QString("00:00:00"));
  96. statusWidget->ui->streamTime->setDisabled(true);
  97. statusWidget->ui->streamIcon->setPixmap(
  98. streamingInactivePixmap);
  99. statusWidget->ui->statusIcon->setPixmap(inactivePixmap);
  100. statusWidget->ui->delayFrame->hide();
  101. statusWidget->ui->issuesFrame->hide();
  102. statusWidget->ui->kbps->hide();
  103. totalStreamSeconds = 0;
  104. congestionArray.clear();
  105. disconnected = false;
  106. firstCongestionUpdate = false;
  107. }
  108. if (!recordOutput) {
  109. statusWidget->ui->recordTime->setText(QString("00:00:00"));
  110. statusWidget->ui->recordTime->setDisabled(true);
  111. statusWidget->ui->recordIcon->setPixmap(
  112. recordingInactivePixmap);
  113. totalRecordSeconds = 0;
  114. }
  115. if (main->outputHandler && !main->outputHandler->Active()) {
  116. delete refreshTimer;
  117. statusWidget->ui->delayInfo->setText("");
  118. statusWidget->ui->droppedFrames->setText(
  119. QTStr("DroppedFrames").arg("0", "0.0"));
  120. statusWidget->ui->kbps->setText("0 kbps");
  121. delaySecTotal = 0;
  122. delaySecStarting = 0;
  123. delaySecStopping = 0;
  124. reconnectTimeout = 0;
  125. active = false;
  126. overloadedNotify = true;
  127. statusWidget->ui->statusIcon->setPixmap(inactivePixmap);
  128. }
  129. }
  130. void OBSBasicStatusBar::UpdateDelayMsg()
  131. {
  132. QString msg;
  133. if (delaySecTotal) {
  134. if (delaySecStarting && !delaySecStopping) {
  135. msg = QTStr("Basic.StatusBar.DelayStartingIn");
  136. msg = msg.arg(QString::number(delaySecStarting));
  137. } else if (!delaySecStarting && delaySecStopping) {
  138. msg = QTStr("Basic.StatusBar.DelayStoppingIn");
  139. msg = msg.arg(QString::number(delaySecStopping));
  140. } else if (delaySecStarting && delaySecStopping) {
  141. msg = QTStr("Basic.StatusBar.DelayStartingStoppingIn");
  142. msg = msg.arg(QString::number(delaySecStopping),
  143. QString::number(delaySecStarting));
  144. } else {
  145. msg = QTStr("Basic.StatusBar.Delay");
  146. msg = msg.arg(QString::number(delaySecTotal));
  147. }
  148. if (!statusWidget->ui->delayFrame->isVisible())
  149. statusWidget->ui->delayFrame->show();
  150. statusWidget->ui->delayInfo->setText(msg);
  151. }
  152. }
  153. void OBSBasicStatusBar::UpdateBandwidth()
  154. {
  155. if (!streamOutput)
  156. return;
  157. if (++seconds < bitrateUpdateSeconds)
  158. return;
  159. uint64_t bytesSent = obs_output_get_total_bytes(streamOutput);
  160. uint64_t bytesSentTime = os_gettime_ns();
  161. if (bytesSent < lastBytesSent)
  162. bytesSent = 0;
  163. if (bytesSent == 0)
  164. lastBytesSent = 0;
  165. uint64_t bitsBetween = (bytesSent - lastBytesSent) * 8;
  166. double timePassed =
  167. double(bytesSentTime - lastBytesSentTime) / 1000000000.0;
  168. double kbitsPerSec = double(bitsBetween) / timePassed / 1000.0;
  169. QString text;
  170. text += QString::number(kbitsPerSec, 'f', 0) + QString(" kbps");
  171. statusWidget->ui->kbps->setText(text);
  172. statusWidget->ui->kbps->setMinimumWidth(
  173. statusWidget->ui->kbps->width());
  174. if (!statusWidget->ui->kbps->isVisible())
  175. statusWidget->ui->kbps->show();
  176. lastBytesSent = bytesSent;
  177. lastBytesSentTime = bytesSentTime;
  178. seconds = 0;
  179. }
  180. void OBSBasicStatusBar::UpdateCPUUsage()
  181. {
  182. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  183. if (!main)
  184. return;
  185. QString text;
  186. text += QString("CPU: ") +
  187. QString::number(main->GetCPUUsage(), 'f', 1) + QString("%");
  188. statusWidget->ui->cpuUsage->setText(text);
  189. statusWidget->ui->cpuUsage->setMinimumWidth(
  190. statusWidget->ui->cpuUsage->width());
  191. UpdateCurrentFPS();
  192. }
  193. void OBSBasicStatusBar::UpdateCurrentFPS()
  194. {
  195. struct obs_video_info ovi;
  196. obs_get_video_info(&ovi);
  197. float targetFPS = (float)ovi.fps_num / (float)ovi.fps_den;
  198. QString text = QString::asprintf("%.2f / %.2f FPS",
  199. obs_get_active_fps(), targetFPS);
  200. statusWidget->ui->fpsCurrent->setText(text);
  201. statusWidget->ui->fpsCurrent->setMinimumWidth(
  202. statusWidget->ui->fpsCurrent->width());
  203. }
  204. void OBSBasicStatusBar::UpdateStreamTime()
  205. {
  206. totalStreamSeconds++;
  207. int seconds = totalStreamSeconds % 60;
  208. int totalMinutes = totalStreamSeconds / 60;
  209. int minutes = totalMinutes % 60;
  210. int hours = totalMinutes / 60;
  211. QString text =
  212. QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds);
  213. statusWidget->ui->streamTime->setText(text);
  214. if (streamOutput && !statusWidget->ui->streamTime->isEnabled())
  215. statusWidget->ui->streamTime->setDisabled(false);
  216. if (reconnectTimeout > 0) {
  217. QString msg = QTStr("Basic.StatusBar.Reconnecting")
  218. .arg(QString::number(retries),
  219. QString::number(reconnectTimeout));
  220. showMessage(msg);
  221. disconnected = true;
  222. statusWidget->ui->statusIcon->setPixmap(disconnectedPixmap);
  223. congestionArray.clear();
  224. reconnectTimeout--;
  225. } else if (retries > 0) {
  226. QString msg = QTStr("Basic.StatusBar.AttemptingReconnect");
  227. showMessage(msg.arg(QString::number(retries)));
  228. }
  229. if (delaySecStopping > 0 || delaySecStarting > 0) {
  230. if (delaySecStopping > 0)
  231. --delaySecStopping;
  232. if (delaySecStarting > 0)
  233. --delaySecStarting;
  234. UpdateDelayMsg();
  235. }
  236. }
  237. extern volatile bool recording_paused;
  238. void OBSBasicStatusBar::UpdateRecordTime()
  239. {
  240. bool paused = os_atomic_load_bool(&recording_paused);
  241. if (!paused) {
  242. totalRecordSeconds++;
  243. int seconds = totalRecordSeconds % 60;
  244. int totalMinutes = totalRecordSeconds / 60;
  245. int minutes = totalMinutes % 60;
  246. int hours = totalMinutes / 60;
  247. QString text = QString::asprintf("%02d:%02d:%02d", hours,
  248. minutes, seconds);
  249. statusWidget->ui->recordTime->setText(text);
  250. if (recordOutput && !statusWidget->ui->recordTime->isEnabled())
  251. statusWidget->ui->recordTime->setDisabled(false);
  252. } else {
  253. statusWidget->ui->recordIcon->setPixmap(
  254. streamPauseIconToggle ? recordingPauseInactivePixmap
  255. : recordingPausePixmap);
  256. streamPauseIconToggle = !streamPauseIconToggle;
  257. }
  258. }
  259. void OBSBasicStatusBar::UpdateDroppedFrames()
  260. {
  261. if (!streamOutput)
  262. return;
  263. int totalDropped = obs_output_get_frames_dropped(streamOutput);
  264. int totalFrames = obs_output_get_total_frames(streamOutput);
  265. double percent = (double)totalDropped / (double)totalFrames * 100.0;
  266. if (!totalFrames)
  267. return;
  268. QString text = QTStr("DroppedFrames");
  269. text = text.arg(QString::number(totalDropped),
  270. QString::number(percent, 'f', 1));
  271. statusWidget->ui->droppedFrames->setText(text);
  272. if (!statusWidget->ui->issuesFrame->isVisible())
  273. statusWidget->ui->issuesFrame->show();
  274. /* ----------------------------------- *
  275. * calculate congestion color */
  276. float congestion = obs_output_get_congestion(streamOutput);
  277. float avgCongestion = (congestion + lastCongestion) * 0.5f;
  278. if (avgCongestion < congestion)
  279. avgCongestion = congestion;
  280. if (avgCongestion > 1.0f)
  281. avgCongestion = 1.0f;
  282. lastCongestion = congestion;
  283. if (disconnected)
  284. return;
  285. bool update = firstCongestionUpdate;
  286. float congestionOverTime = avgCongestion;
  287. if (congestionArray.size() >= congestionUpdateSeconds) {
  288. congestionOverTime = accumulate(congestionArray.begin(),
  289. congestionArray.end(), 0.0f) /
  290. (float)congestionArray.size();
  291. congestionArray.clear();
  292. update = true;
  293. } else {
  294. congestionArray.emplace_back(avgCongestion);
  295. }
  296. if (update) {
  297. if (congestionOverTime <= excellentThreshold + EPSILON)
  298. statusWidget->ui->statusIcon->setPixmap(
  299. excellentPixmap);
  300. else if (congestionOverTime <= goodThreshold)
  301. statusWidget->ui->statusIcon->setPixmap(goodPixmap);
  302. else if (congestionOverTime <= mediocreThreshold)
  303. statusWidget->ui->statusIcon->setPixmap(mediocrePixmap);
  304. else if (congestionOverTime <= badThreshold)
  305. statusWidget->ui->statusIcon->setPixmap(badPixmap);
  306. firstCongestionUpdate = false;
  307. }
  308. }
  309. void OBSBasicStatusBar::OBSOutputReconnect(void *data, calldata_t *params)
  310. {
  311. OBSBasicStatusBar *statusBar =
  312. reinterpret_cast<OBSBasicStatusBar *>(data);
  313. int seconds = (int)calldata_int(params, "timeout_sec");
  314. QMetaObject::invokeMethod(statusBar, "Reconnect", Q_ARG(int, seconds));
  315. }
  316. void OBSBasicStatusBar::OBSOutputReconnectSuccess(void *data, calldata_t *)
  317. {
  318. OBSBasicStatusBar *statusBar =
  319. reinterpret_cast<OBSBasicStatusBar *>(data);
  320. QMetaObject::invokeMethod(statusBar, "ReconnectSuccess");
  321. }
  322. void OBSBasicStatusBar::Reconnect(int seconds)
  323. {
  324. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  325. if (!retries)
  326. main->SysTrayNotify(
  327. QTStr("Basic.SystemTray.Message.Reconnecting"),
  328. QSystemTrayIcon::Warning);
  329. reconnectTimeout = seconds;
  330. if (streamOutput) {
  331. delaySecTotal = obs_output_get_active_delay(streamOutput);
  332. UpdateDelayMsg();
  333. retries++;
  334. }
  335. }
  336. void OBSBasicStatusBar::ReconnectClear()
  337. {
  338. retries = 0;
  339. reconnectTimeout = 0;
  340. seconds = -1;
  341. lastBytesSent = 0;
  342. lastBytesSentTime = os_gettime_ns();
  343. delaySecTotal = 0;
  344. UpdateDelayMsg();
  345. }
  346. void OBSBasicStatusBar::ReconnectSuccess()
  347. {
  348. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  349. QString msg = QTStr("Basic.StatusBar.ReconnectSuccessful");
  350. showMessage(msg, 4000);
  351. main->SysTrayNotify(msg, QSystemTrayIcon::Information);
  352. ReconnectClear();
  353. if (streamOutput) {
  354. delaySecTotal = obs_output_get_active_delay(streamOutput);
  355. UpdateDelayMsg();
  356. disconnected = false;
  357. firstCongestionUpdate = true;
  358. }
  359. }
  360. void OBSBasicStatusBar::UpdateStatusBar()
  361. {
  362. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  363. UpdateBandwidth();
  364. if (streamOutput)
  365. UpdateStreamTime();
  366. if (recordOutput)
  367. UpdateRecordTime();
  368. UpdateDroppedFrames();
  369. int skipped = video_output_get_skipped_frames(obs_get_video());
  370. int total = video_output_get_total_frames(obs_get_video());
  371. skipped -= startSkippedFrameCount;
  372. total -= startTotalFrameCount;
  373. int diff = skipped - lastSkippedFrameCount;
  374. double percentage = double(skipped) / double(total) * 100.0;
  375. if (diff > 10 && percentage >= 0.1f) {
  376. showMessage(QTStr("HighResourceUsage"), 4000);
  377. if (!main->isVisible() && overloadedNotify) {
  378. main->SysTrayNotify(QTStr("HighResourceUsage"),
  379. QSystemTrayIcon::Warning);
  380. overloadedNotify = false;
  381. }
  382. }
  383. lastSkippedFrameCount = skipped;
  384. }
  385. void OBSBasicStatusBar::StreamDelayStarting(int sec)
  386. {
  387. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  388. if (!main || !main->outputHandler)
  389. return;
  390. streamOutput = main->outputHandler->streamOutput;
  391. delaySecTotal = delaySecStarting = sec;
  392. UpdateDelayMsg();
  393. Activate();
  394. }
  395. void OBSBasicStatusBar::StreamDelayStopping(int sec)
  396. {
  397. delaySecTotal = delaySecStopping = sec;
  398. UpdateDelayMsg();
  399. }
  400. void OBSBasicStatusBar::StreamStarted(obs_output_t *output)
  401. {
  402. streamOutput = output;
  403. signal_handler_connect(obs_output_get_signal_handler(streamOutput),
  404. "reconnect", OBSOutputReconnect, this);
  405. signal_handler_connect(obs_output_get_signal_handler(streamOutput),
  406. "reconnect_success", OBSOutputReconnectSuccess,
  407. this);
  408. retries = 0;
  409. lastBytesSent = 0;
  410. lastBytesSentTime = os_gettime_ns();
  411. Activate();
  412. }
  413. void OBSBasicStatusBar::StreamStopped()
  414. {
  415. if (streamOutput) {
  416. signal_handler_disconnect(
  417. obs_output_get_signal_handler(streamOutput),
  418. "reconnect", OBSOutputReconnect, this);
  419. signal_handler_disconnect(
  420. obs_output_get_signal_handler(streamOutput),
  421. "reconnect_success", OBSOutputReconnectSuccess, this);
  422. ReconnectClear();
  423. streamOutput = nullptr;
  424. clearMessage();
  425. Deactivate();
  426. }
  427. }
  428. void OBSBasicStatusBar::RecordingStarted(obs_output_t *output)
  429. {
  430. recordOutput = output;
  431. Activate();
  432. }
  433. void OBSBasicStatusBar::RecordingStopped()
  434. {
  435. recordOutput = nullptr;
  436. Deactivate();
  437. }
  438. void OBSBasicStatusBar::RecordingPaused()
  439. {
  440. QString text = statusWidget->ui->recordTime->text() +
  441. QStringLiteral(" (PAUSED)");
  442. statusWidget->ui->recordTime->setText(text);
  443. if (recordOutput) {
  444. statusWidget->ui->recordIcon->setPixmap(recordingPausePixmap);
  445. streamPauseIconToggle = true;
  446. }
  447. }
  448. void OBSBasicStatusBar::RecordingUnpaused()
  449. {
  450. if (recordOutput) {
  451. statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap);
  452. }
  453. }
  454. static QPixmap GetPixmap(const QString &filename)
  455. {
  456. bool darkTheme = obs_frontend_is_theme_dark();
  457. QString path;
  458. if (darkTheme) {
  459. std::string darkPath;
  460. QString themePath = QString("themes/Dark/") + filename;
  461. GetDataFilePath(QT_TO_UTF8(themePath), darkPath);
  462. path = QT_UTF8(darkPath.c_str());
  463. } else {
  464. path = QString(":/res/images/" + filename);
  465. }
  466. return QIcon(path).pixmap(QSize(16, 16));
  467. }
  468. void OBSBasicStatusBar::UpdateIcons()
  469. {
  470. disconnectedPixmap = GetPixmap("network-disconnected.svg");
  471. inactivePixmap = GetPixmap("network-inactive.svg");
  472. streamingInactivePixmap = GetPixmap("streaming-inactive.svg");
  473. recordingInactivePixmap = GetPixmap("recording-inactive.svg");
  474. recordingPauseInactivePixmap =
  475. GetPixmap("recording-pause-inactive.svg");
  476. bool streaming = obs_frontend_streaming_active();
  477. if (!streaming) {
  478. statusWidget->ui->streamIcon->setPixmap(
  479. streamingInactivePixmap);
  480. statusWidget->ui->statusIcon->setPixmap(inactivePixmap);
  481. } else {
  482. if (disconnected)
  483. statusWidget->ui->statusIcon->setPixmap(
  484. disconnectedPixmap);
  485. }
  486. bool recording = obs_frontend_recording_active();
  487. if (!recording)
  488. statusWidget->ui->recordIcon->setPixmap(
  489. recordingInactivePixmap);
  490. }