window-basic-status-bar.cpp 16 KB

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