window-basic-status-bar.cpp 16 KB

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