window-basic-status-bar.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  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, SIGNAL(timeout()), this,
  63. SLOT(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. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  196. if (!main)
  197. return;
  198. struct obs_video_info ovi;
  199. obs_get_video_info(&ovi);
  200. float targetFPS = (float)ovi.fps_num / (float)ovi.fps_den;
  201. QString text;
  202. text += QString::number(obs_get_active_fps(), 'f', 2);
  203. text += QString(" / ");
  204. text += QString::number(targetFPS, 'f', 2);
  205. text += QString(" FPS");
  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. int seconds = totalRecordSeconds % 60;
  250. int totalMinutes = totalRecordSeconds / 60;
  251. int minutes = totalMinutes % 60;
  252. int hours = totalMinutes / 60;
  253. QString text = QString::asprintf("%02d:%02d:%02d", hours,
  254. minutes, seconds);
  255. statusWidget->ui->recordTime->setText(text);
  256. if (recordOutput && !statusWidget->ui->recordTime->isEnabled())
  257. statusWidget->ui->recordTime->setDisabled(false);
  258. } else {
  259. statusWidget->ui->recordIcon->setPixmap(
  260. streamPauseIconToggle ? recordingPauseInactivePixmap
  261. : recordingPausePixmap);
  262. streamPauseIconToggle = !streamPauseIconToggle;
  263. }
  264. }
  265. void OBSBasicStatusBar::UpdateDroppedFrames()
  266. {
  267. if (!streamOutput)
  268. return;
  269. int totalDropped = obs_output_get_frames_dropped(streamOutput);
  270. int totalFrames = obs_output_get_total_frames(streamOutput);
  271. double percent = (double)totalDropped / (double)totalFrames * 100.0;
  272. if (!totalFrames)
  273. return;
  274. QString text = QTStr("DroppedFrames");
  275. text = text.arg(QString::number(totalDropped),
  276. QString::number(percent, 'f', 1));
  277. statusWidget->ui->droppedFrames->setText(text);
  278. if (!statusWidget->ui->issuesFrame->isVisible())
  279. statusWidget->ui->issuesFrame->show();
  280. /* ----------------------------------- *
  281. * calculate congestion color */
  282. float congestion = obs_output_get_congestion(streamOutput);
  283. float avgCongestion = (congestion + lastCongestion) * 0.5f;
  284. if (avgCongestion < congestion)
  285. avgCongestion = congestion;
  286. if (avgCongestion > 1.0f)
  287. avgCongestion = 1.0f;
  288. lastCongestion = congestion;
  289. if (disconnected)
  290. return;
  291. bool update = firstCongestionUpdate;
  292. float congestionOverTime = avgCongestion;
  293. if (congestionArray.size() >= congestionUpdateSeconds) {
  294. congestionOverTime = accumulate(congestionArray.begin(),
  295. congestionArray.end(), 0.0f) /
  296. (float)congestionArray.size();
  297. congestionArray.clear();
  298. update = true;
  299. } else {
  300. congestionArray.emplace_back(avgCongestion);
  301. }
  302. if (update) {
  303. if (congestionOverTime <= excellentThreshold + EPSILON)
  304. statusWidget->ui->statusIcon->setPixmap(
  305. excellentPixmap);
  306. else if (congestionOverTime <= goodThreshold)
  307. statusWidget->ui->statusIcon->setPixmap(goodPixmap);
  308. else if (congestionOverTime <= mediocreThreshold)
  309. statusWidget->ui->statusIcon->setPixmap(mediocrePixmap);
  310. else if (congestionOverTime <= badThreshold)
  311. statusWidget->ui->statusIcon->setPixmap(badPixmap);
  312. firstCongestionUpdate = false;
  313. }
  314. }
  315. void OBSBasicStatusBar::OBSOutputReconnect(void *data, calldata_t *params)
  316. {
  317. OBSBasicStatusBar *statusBar =
  318. reinterpret_cast<OBSBasicStatusBar *>(data);
  319. int seconds = (int)calldata_int(params, "timeout_sec");
  320. QMetaObject::invokeMethod(statusBar, "Reconnect", Q_ARG(int, seconds));
  321. }
  322. void OBSBasicStatusBar::OBSOutputReconnectSuccess(void *data, calldata_t *)
  323. {
  324. OBSBasicStatusBar *statusBar =
  325. reinterpret_cast<OBSBasicStatusBar *>(data);
  326. QMetaObject::invokeMethod(statusBar, "ReconnectSuccess");
  327. }
  328. void OBSBasicStatusBar::Reconnect(int seconds)
  329. {
  330. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  331. if (!retries)
  332. main->SysTrayNotify(
  333. QTStr("Basic.SystemTray.Message.Reconnecting"),
  334. QSystemTrayIcon::Warning);
  335. reconnectTimeout = seconds;
  336. if (streamOutput) {
  337. delaySecTotal = obs_output_get_active_delay(streamOutput);
  338. UpdateDelayMsg();
  339. retries++;
  340. }
  341. }
  342. void OBSBasicStatusBar::ReconnectClear()
  343. {
  344. retries = 0;
  345. reconnectTimeout = 0;
  346. seconds = -1;
  347. lastBytesSent = 0;
  348. lastBytesSentTime = os_gettime_ns();
  349. delaySecTotal = 0;
  350. UpdateDelayMsg();
  351. }
  352. void OBSBasicStatusBar::ReconnectSuccess()
  353. {
  354. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  355. QString msg = QTStr("Basic.StatusBar.ReconnectSuccessful");
  356. showMessage(msg, 4000);
  357. main->SysTrayNotify(msg, QSystemTrayIcon::Information);
  358. ReconnectClear();
  359. if (streamOutput) {
  360. delaySecTotal = obs_output_get_active_delay(streamOutput);
  361. UpdateDelayMsg();
  362. disconnected = false;
  363. firstCongestionUpdate = true;
  364. }
  365. }
  366. void OBSBasicStatusBar::UpdateStatusBar()
  367. {
  368. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  369. UpdateBandwidth();
  370. if (streamOutput)
  371. UpdateStreamTime();
  372. if (recordOutput)
  373. UpdateRecordTime();
  374. UpdateDroppedFrames();
  375. int skipped = video_output_get_skipped_frames(obs_get_video());
  376. int total = video_output_get_total_frames(obs_get_video());
  377. skipped -= startSkippedFrameCount;
  378. total -= startTotalFrameCount;
  379. int diff = skipped - lastSkippedFrameCount;
  380. double percentage = double(skipped) / double(total) * 100.0;
  381. if (diff > 10 && percentage >= 0.1f) {
  382. showMessage(QTStr("HighResourceUsage"), 4000);
  383. if (!main->isVisible() && overloadedNotify) {
  384. main->SysTrayNotify(QTStr("HighResourceUsage"),
  385. QSystemTrayIcon::Warning);
  386. overloadedNotify = false;
  387. }
  388. }
  389. lastSkippedFrameCount = skipped;
  390. }
  391. void OBSBasicStatusBar::StreamDelayStarting(int sec)
  392. {
  393. OBSBasic *main = qobject_cast<OBSBasic *>(parent());
  394. if (!main || !main->outputHandler)
  395. return;
  396. streamOutput = main->outputHandler->streamOutput;
  397. delaySecTotal = delaySecStarting = sec;
  398. UpdateDelayMsg();
  399. Activate();
  400. }
  401. void OBSBasicStatusBar::StreamDelayStopping(int sec)
  402. {
  403. delaySecTotal = delaySecStopping = sec;
  404. UpdateDelayMsg();
  405. }
  406. void OBSBasicStatusBar::StreamStarted(obs_output_t *output)
  407. {
  408. streamOutput = output;
  409. signal_handler_connect(obs_output_get_signal_handler(streamOutput),
  410. "reconnect", OBSOutputReconnect, this);
  411. signal_handler_connect(obs_output_get_signal_handler(streamOutput),
  412. "reconnect_success", OBSOutputReconnectSuccess,
  413. this);
  414. retries = 0;
  415. lastBytesSent = 0;
  416. lastBytesSentTime = os_gettime_ns();
  417. Activate();
  418. }
  419. void OBSBasicStatusBar::StreamStopped()
  420. {
  421. if (streamOutput) {
  422. signal_handler_disconnect(
  423. obs_output_get_signal_handler(streamOutput),
  424. "reconnect", OBSOutputReconnect, this);
  425. signal_handler_disconnect(
  426. obs_output_get_signal_handler(streamOutput),
  427. "reconnect_success", OBSOutputReconnectSuccess, this);
  428. ReconnectClear();
  429. streamOutput = nullptr;
  430. clearMessage();
  431. Deactivate();
  432. }
  433. }
  434. void OBSBasicStatusBar::RecordingStarted(obs_output_t *output)
  435. {
  436. recordOutput = output;
  437. Activate();
  438. }
  439. void OBSBasicStatusBar::RecordingStopped()
  440. {
  441. recordOutput = nullptr;
  442. Deactivate();
  443. }
  444. void OBSBasicStatusBar::RecordingPaused()
  445. {
  446. QString text = statusWidget->ui->recordTime->text() +
  447. QStringLiteral(" (PAUSED)");
  448. statusWidget->ui->recordTime->setText(text);
  449. if (recordOutput) {
  450. statusWidget->ui->recordIcon->setPixmap(recordingPausePixmap);
  451. streamPauseIconToggle = true;
  452. }
  453. }
  454. void OBSBasicStatusBar::RecordingUnpaused()
  455. {
  456. if (recordOutput) {
  457. statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap);
  458. }
  459. }
  460. static QPixmap GetPixmap(const QString &filename)
  461. {
  462. bool darkTheme = obs_frontend_is_theme_dark();
  463. QString path;
  464. if (darkTheme) {
  465. std::string darkPath;
  466. QString themePath = QString("themes/Dark/") + filename;
  467. GetDataFilePath(QT_TO_UTF8(themePath), darkPath);
  468. path = QT_UTF8(darkPath.c_str());
  469. } else {
  470. path = QString(":/res/images/" + filename);
  471. }
  472. return QIcon(path).pixmap(QSize(16, 16));
  473. }
  474. void OBSBasicStatusBar::UpdateIcons()
  475. {
  476. disconnectedPixmap = GetPixmap("network-disconnected.svg");
  477. inactivePixmap = GetPixmap("network-inactive.svg");
  478. streamingInactivePixmap = GetPixmap("streaming-inactive.svg");
  479. recordingInactivePixmap = GetPixmap("recording-inactive.svg");
  480. recordingPauseInactivePixmap =
  481. GetPixmap("recording-pause-inactive.svg");
  482. bool streaming = obs_frontend_streaming_active();
  483. if (!streaming) {
  484. statusWidget->ui->streamIcon->setPixmap(
  485. streamingInactivePixmap);
  486. statusWidget->ui->statusIcon->setPixmap(inactivePixmap);
  487. } else {
  488. if (disconnected)
  489. statusWidget->ui->statusIcon->setPixmap(
  490. disconnectedPixmap);
  491. }
  492. bool recording = obs_frontend_recording_active();
  493. if (!recording)
  494. statusWidget->ui->recordIcon->setPixmap(
  495. recordingInactivePixmap);
  496. }