window-basic-status-bar.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  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. uint64_t bytesSent = obs_output_get_total_bytes(streamOutput);
  166. uint64_t bytesSentTime = os_gettime_ns();
  167. if (bytesSent < lastBytesSent)
  168. bytesSent = 0;
  169. if (bytesSent == 0)
  170. lastBytesSent = 0;
  171. uint64_t bitsBetween = (bytesSent - lastBytesSent) * 8;
  172. double timePassed =
  173. double(bytesSentTime - lastBytesSentTime) / 1000000000.0;
  174. double kbitsPerSec = double(bitsBetween) / timePassed / 1000.0;
  175. QString text;
  176. text += QString::number(kbitsPerSec, 'f', 0) + QString(" kbps");
  177. statusWidget->ui->kbps->setText(text);
  178. statusWidget->ui->kbps->setMinimumWidth(
  179. statusWidget->ui->kbps->width());
  180. if (!statusWidget->ui->kbps->isVisible())
  181. statusWidget->ui->kbps->show();
  182. lastBytesSent = bytesSent;
  183. lastBytesSentTime = bytesSentTime;
  184. seconds = 0;
  185. }
  186. void OBSBasicStatusBar::UpdateCPUUsage()
  187. {
  188. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  189. if (!main)
  190. return;
  191. QString text;
  192. text += QString("CPU: ") +
  193. QString::number(main->GetCPUUsage(), 'f', 1) + QString("%");
  194. statusWidget->ui->cpuUsage->setText(text);
  195. statusWidget->ui->cpuUsage->setMinimumWidth(
  196. statusWidget->ui->cpuUsage->width());
  197. UpdateCurrentFPS();
  198. }
  199. void OBSBasicStatusBar::UpdateCurrentFPS()
  200. {
  201. struct obs_video_info ovi;
  202. obs_get_video_info(&ovi);
  203. float targetFPS = (float)ovi.fps_num / (float)ovi.fps_den;
  204. QString text = QString::asprintf("%.2f / %.2f FPS",
  205. obs_get_active_fps(), targetFPS);
  206. statusWidget->ui->fpsCurrent->setText(text);
  207. statusWidget->ui->fpsCurrent->setMinimumWidth(
  208. statusWidget->ui->fpsCurrent->width());
  209. }
  210. void OBSBasicStatusBar::UpdateStreamTime()
  211. {
  212. totalStreamSeconds++;
  213. int seconds = totalStreamSeconds % 60;
  214. int totalMinutes = totalStreamSeconds / 60;
  215. int minutes = totalMinutes % 60;
  216. int hours = totalMinutes / 60;
  217. QString text =
  218. QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds);
  219. statusWidget->ui->streamTime->setText(text);
  220. if (streamOutput && !statusWidget->ui->streamTime->isEnabled())
  221. statusWidget->ui->streamTime->setDisabled(false);
  222. if (reconnectTimeout > 0) {
  223. QString msg = QTStr("Basic.StatusBar.Reconnecting")
  224. .arg(QString::number(retries),
  225. QString::number(reconnectTimeout));
  226. showMessage(msg);
  227. disconnected = true;
  228. statusWidget->ui->statusIcon->setPixmap(disconnectedPixmap);
  229. congestionArray.clear();
  230. reconnectTimeout--;
  231. } else if (retries > 0) {
  232. QString msg = QTStr("Basic.StatusBar.AttemptingReconnect");
  233. showMessage(msg.arg(QString::number(retries)));
  234. }
  235. if (delaySecStopping > 0 || delaySecStarting > 0) {
  236. if (delaySecStopping > 0)
  237. --delaySecStopping;
  238. if (delaySecStarting > 0)
  239. --delaySecStarting;
  240. UpdateDelayMsg();
  241. }
  242. }
  243. extern volatile bool recording_paused;
  244. void OBSBasicStatusBar::UpdateRecordTime()
  245. {
  246. bool paused = os_atomic_load_bool(&recording_paused);
  247. if (!paused) {
  248. totalRecordSeconds++;
  249. if (recordOutput && !statusWidget->ui->recordTime->isEnabled())
  250. statusWidget->ui->recordTime->setDisabled(false);
  251. } else {
  252. statusWidget->ui->recordIcon->setPixmap(
  253. streamPauseIconToggle ? recordingPauseInactivePixmap
  254. : recordingPausePixmap);
  255. streamPauseIconToggle = !streamPauseIconToggle;
  256. }
  257. UpdateRecordTimeLabel();
  258. }
  259. void OBSBasicStatusBar::UpdateRecordTimeLabel()
  260. {
  261. int seconds = totalRecordSeconds % 60;
  262. int totalMinutes = totalRecordSeconds / 60;
  263. int minutes = totalMinutes % 60;
  264. int hours = totalMinutes / 60;
  265. QString text =
  266. QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds);
  267. if (os_atomic_load_bool(&recording_paused)) {
  268. text += QStringLiteral(" (PAUSED)");
  269. }
  270. statusWidget->ui->recordTime->setText(text);
  271. }
  272. void OBSBasicStatusBar::UpdateDroppedFrames()
  273. {
  274. if (!streamOutput)
  275. return;
  276. int totalDropped = obs_output_get_frames_dropped(streamOutput);
  277. int totalFrames = obs_output_get_total_frames(streamOutput);
  278. double percent = (double)totalDropped / (double)totalFrames * 100.0;
  279. if (!totalFrames)
  280. return;
  281. QString text = QTStr("DroppedFrames");
  282. text = text.arg(QString::number(totalDropped),
  283. QString::number(percent, 'f', 1));
  284. statusWidget->ui->droppedFrames->setText(text);
  285. if (!statusWidget->ui->issuesFrame->isVisible())
  286. statusWidget->ui->issuesFrame->show();
  287. /* ----------------------------------- *
  288. * calculate congestion color */
  289. float congestion = obs_output_get_congestion(streamOutput);
  290. float avgCongestion = (congestion + lastCongestion) * 0.5f;
  291. if (avgCongestion < congestion)
  292. avgCongestion = congestion;
  293. if (avgCongestion > 1.0f)
  294. avgCongestion = 1.0f;
  295. lastCongestion = congestion;
  296. if (disconnected)
  297. return;
  298. bool update = firstCongestionUpdate;
  299. float congestionOverTime = avgCongestion;
  300. if (congestionArray.size() >= congestionUpdateSeconds) {
  301. congestionOverTime = accumulate(congestionArray.begin(),
  302. congestionArray.end(), 0.0f) /
  303. (float)congestionArray.size();
  304. congestionArray.clear();
  305. update = true;
  306. } else {
  307. congestionArray.emplace_back(avgCongestion);
  308. }
  309. if (update) {
  310. if (congestionOverTime <= excellentThreshold + EPSILON)
  311. statusWidget->ui->statusIcon->setPixmap(
  312. excellentPixmap);
  313. else if (congestionOverTime <= goodThreshold)
  314. statusWidget->ui->statusIcon->setPixmap(goodPixmap);
  315. else if (congestionOverTime <= mediocreThreshold)
  316. statusWidget->ui->statusIcon->setPixmap(mediocrePixmap);
  317. else if (congestionOverTime <= badThreshold)
  318. statusWidget->ui->statusIcon->setPixmap(badPixmap);
  319. firstCongestionUpdate = false;
  320. }
  321. }
  322. void OBSBasicStatusBar::OBSOutputReconnect(void *data, calldata_t *params)
  323. {
  324. OBSBasicStatusBar *statusBar =
  325. reinterpret_cast<OBSBasicStatusBar *>(data);
  326. int seconds = (int)calldata_int(params, "timeout_sec");
  327. QMetaObject::invokeMethod(statusBar, "Reconnect", Q_ARG(int, seconds));
  328. }
  329. void OBSBasicStatusBar::OBSOutputReconnectSuccess(void *data, calldata_t *)
  330. {
  331. OBSBasicStatusBar *statusBar =
  332. reinterpret_cast<OBSBasicStatusBar *>(data);
  333. QMetaObject::invokeMethod(statusBar, "ReconnectSuccess");
  334. }
  335. void OBSBasicStatusBar::Reconnect(int seconds)
  336. {
  337. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  338. if (!retries)
  339. main->SysTrayNotify(
  340. QTStr("Basic.SystemTray.Message.Reconnecting"),
  341. QSystemTrayIcon::Warning);
  342. reconnectTimeout = seconds;
  343. if (streamOutput) {
  344. delaySecTotal = obs_output_get_active_delay(streamOutput);
  345. UpdateDelayMsg();
  346. retries++;
  347. }
  348. }
  349. void OBSBasicStatusBar::ReconnectClear()
  350. {
  351. retries = 0;
  352. reconnectTimeout = 0;
  353. seconds = -1;
  354. lastBytesSent = 0;
  355. lastBytesSentTime = os_gettime_ns();
  356. delaySecTotal = 0;
  357. UpdateDelayMsg();
  358. }
  359. void OBSBasicStatusBar::ReconnectSuccess()
  360. {
  361. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  362. QString msg = QTStr("Basic.StatusBar.ReconnectSuccessful");
  363. showMessage(msg, 4000);
  364. main->SysTrayNotify(msg, QSystemTrayIcon::Information);
  365. ReconnectClear();
  366. if (streamOutput) {
  367. delaySecTotal = obs_output_get_active_delay(streamOutput);
  368. UpdateDelayMsg();
  369. disconnected = false;
  370. firstCongestionUpdate = true;
  371. }
  372. }
  373. void OBSBasicStatusBar::UpdateStatusBar()
  374. {
  375. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  376. UpdateBandwidth();
  377. if (streamOutput)
  378. UpdateStreamTime();
  379. if (recordOutput)
  380. UpdateRecordTime();
  381. UpdateDroppedFrames();
  382. int skipped = video_output_get_skipped_frames(obs_get_video());
  383. int total = video_output_get_total_frames(obs_get_video());
  384. skipped -= startSkippedFrameCount;
  385. total -= startTotalFrameCount;
  386. int diff = skipped - lastSkippedFrameCount;
  387. double percentage = double(skipped) / double(total) * 100.0;
  388. if (diff > 10 && percentage >= 0.1f) {
  389. showMessage(QTStr("HighResourceUsage"), 4000);
  390. if (!main->isVisible() && overloadedNotify) {
  391. main->SysTrayNotify(QTStr("HighResourceUsage"),
  392. QSystemTrayIcon::Warning);
  393. overloadedNotify = false;
  394. }
  395. }
  396. lastSkippedFrameCount = skipped;
  397. }
  398. void OBSBasicStatusBar::StreamDelayStarting(int sec)
  399. {
  400. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  401. if (!main || !main->outputHandler)
  402. return;
  403. streamOutput = main->outputHandler->streamOutput;
  404. delaySecTotal = delaySecStarting = sec;
  405. UpdateDelayMsg();
  406. Activate();
  407. }
  408. void OBSBasicStatusBar::StreamDelayStopping(int sec)
  409. {
  410. delaySecTotal = delaySecStopping = sec;
  411. UpdateDelayMsg();
  412. }
  413. void OBSBasicStatusBar::StreamStarted(obs_output_t *output)
  414. {
  415. streamOutput = output;
  416. streamSigs.emplace_back(obs_output_get_signal_handler(streamOutput),
  417. "reconnect", OBSOutputReconnect, this);
  418. streamSigs.emplace_back(obs_output_get_signal_handler(streamOutput),
  419. "reconnect_success", OBSOutputReconnectSuccess,
  420. this);
  421. retries = 0;
  422. lastBytesSent = 0;
  423. lastBytesSentTime = os_gettime_ns();
  424. Activate();
  425. }
  426. void OBSBasicStatusBar::StreamStopped()
  427. {
  428. if (streamOutput) {
  429. streamSigs.clear();
  430. ReconnectClear();
  431. streamOutput = nullptr;
  432. clearMessage();
  433. Deactivate();
  434. }
  435. }
  436. void OBSBasicStatusBar::RecordingStarted(obs_output_t *output)
  437. {
  438. recordOutput = output;
  439. Activate();
  440. }
  441. void OBSBasicStatusBar::RecordingStopped()
  442. {
  443. recordOutput = nullptr;
  444. Deactivate();
  445. }
  446. void OBSBasicStatusBar::RecordingPaused()
  447. {
  448. if (recordOutput) {
  449. statusWidget->ui->recordIcon->setPixmap(recordingPausePixmap);
  450. streamPauseIconToggle = true;
  451. }
  452. UpdateRecordTimeLabel();
  453. }
  454. void OBSBasicStatusBar::RecordingUnpaused()
  455. {
  456. if (recordOutput) {
  457. statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap);
  458. }
  459. UpdateRecordTimeLabel();
  460. }
  461. static QPixmap GetPixmap(const QString &filename)
  462. {
  463. QString path = obs_frontend_is_theme_dark() ? "theme:Dark/"
  464. : ":/res/images/";
  465. return QIcon(path + filename).pixmap(QSize(16, 16));
  466. }
  467. void OBSBasicStatusBar::UpdateIcons()
  468. {
  469. disconnectedPixmap = GetPixmap("network-disconnected.svg");
  470. inactivePixmap = GetPixmap("network-inactive.svg");
  471. streamingInactivePixmap = GetPixmap("streaming-inactive.svg");
  472. recordingInactivePixmap = GetPixmap("recording-inactive.svg");
  473. recordingPauseInactivePixmap =
  474. GetPixmap("recording-pause-inactive.svg");
  475. bool streaming = obs_frontend_streaming_active();
  476. if (!streaming) {
  477. statusWidget->ui->streamIcon->setPixmap(
  478. streamingInactivePixmap);
  479. statusWidget->ui->statusIcon->setPixmap(inactivePixmap);
  480. } else {
  481. if (disconnected)
  482. statusWidget->ui->statusIcon->setPixmap(
  483. disconnectedPixmap);
  484. }
  485. bool recording = obs_frontend_recording_active();
  486. if (!recording)
  487. statusWidget->ui->recordIcon->setPixmap(
  488. recordingInactivePixmap);
  489. }
  490. void OBSBasicStatusBar::showMessage(const QString &message, int timeout)
  491. {
  492. messageTimer->stop();
  493. statusWidget->ui->message->setText(message);
  494. if (timeout)
  495. messageTimer->start(timeout);
  496. }
  497. void OBSBasicStatusBar::clearMessage()
  498. {
  499. statusWidget->ui->message->setText("");
  500. }