#include #include #include "obs-app.hpp" #include "window-basic-main.hpp" #include "window-basic-status-bar.hpp" #include "window-basic-main-outputs.hpp" #include "qt-wrappers.hpp" #include "platform.hpp" #include "ui_StatusBarWidget.h" static constexpr int bitrateUpdateSeconds = 2; static constexpr int congestionUpdateSeconds = 4; static constexpr float excellentThreshold = 0.0f; static constexpr float goodThreshold = 0.3333f; static constexpr float mediocreThreshold = 0.6667f; static constexpr float badThreshold = 1.0f; StatusBarWidget::StatusBarWidget(QWidget *parent) : QWidget(parent), ui(new Ui::StatusBarWidget) { ui->setupUi(this); } StatusBarWidget::~StatusBarWidget() {} OBSBasicStatusBar::OBSBasicStatusBar(QWidget *parent) : QStatusBar(parent), excellentPixmap(QIcon(":/res/images/network-excellent.svg") .pixmap(QSize(16, 16))), goodPixmap( QIcon(":/res/images/network-good.svg").pixmap(QSize(16, 16))), mediocrePixmap(QIcon(":/res/images/network-mediocre.svg") .pixmap(QSize(16, 16))), badPixmap( QIcon(":/res/images/network-bad.svg").pixmap(QSize(16, 16))), recordingActivePixmap(QIcon(":/res/images/recording-active.svg") .pixmap(QSize(16, 16))), recordingPausePixmap(QIcon(":/res/images/recording-pause.svg") .pixmap(QSize(16, 16))), streamingActivePixmap(QIcon(":/res/images/streaming-active.svg") .pixmap(QSize(16, 16))) { statusWidget = new StatusBarWidget(this); statusWidget->ui->delayInfo->setText(""); statusWidget->ui->droppedFrames->setText( QTStr("DroppedFrames").arg("0", "0.0")); statusWidget->ui->statusIcon->setPixmap(inactivePixmap); statusWidget->ui->streamIcon->setPixmap(streamingInactivePixmap); statusWidget->ui->streamTime->setDisabled(true); statusWidget->ui->recordIcon->setPixmap(recordingInactivePixmap); statusWidget->ui->recordTime->setDisabled(true); statusWidget->ui->delayFrame->hide(); statusWidget->ui->issuesFrame->hide(); statusWidget->ui->kbps->hide(); addPermanentWidget(statusWidget); setMinimumHeight(statusWidget->height()); UpdateIcons(); connect(App(), &OBSApp::StyleChanged, this, &OBSBasicStatusBar::UpdateIcons); } void OBSBasicStatusBar::Activate() { if (!active) { refreshTimer = new QTimer(this); connect(refreshTimer, SIGNAL(timeout()), this, SLOT(UpdateStatusBar())); int skipped = video_output_get_skipped_frames(obs_get_video()); int total = video_output_get_total_frames(obs_get_video()); totalStreamSeconds = 0; totalRecordSeconds = 0; lastSkippedFrameCount = 0; startSkippedFrameCount = skipped; startTotalFrameCount = total; refreshTimer->start(1000); active = true; if (streamOutput) { statusWidget->ui->statusIcon->setPixmap(inactivePixmap); } } if (streamOutput) { statusWidget->ui->streamIcon->setPixmap(streamingActivePixmap); statusWidget->ui->streamTime->setDisabled(false); statusWidget->ui->issuesFrame->show(); statusWidget->ui->kbps->show(); firstCongestionUpdate = true; } if (recordOutput) { statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap); statusWidget->ui->recordTime->setDisabled(false); } } void OBSBasicStatusBar::Deactivate() { OBSBasic *main = qobject_cast(parent()); if (!main) return; if (!streamOutput) { statusWidget->ui->streamTime->setText(QString("00:00:00")); statusWidget->ui->streamTime->setDisabled(true); statusWidget->ui->streamIcon->setPixmap( streamingInactivePixmap); statusWidget->ui->statusIcon->setPixmap(inactivePixmap); statusWidget->ui->delayFrame->hide(); statusWidget->ui->issuesFrame->hide(); statusWidget->ui->kbps->hide(); totalStreamSeconds = 0; congestionArray.clear(); disconnected = false; firstCongestionUpdate = false; } if (!recordOutput) { statusWidget->ui->recordTime->setText(QString("00:00:00")); statusWidget->ui->recordTime->setDisabled(true); statusWidget->ui->recordIcon->setPixmap( recordingInactivePixmap); totalRecordSeconds = 0; } if (main->outputHandler && !main->outputHandler->Active()) { delete refreshTimer; statusWidget->ui->delayInfo->setText(""); statusWidget->ui->droppedFrames->setText( QTStr("DroppedFrames").arg("0", "0.0")); statusWidget->ui->kbps->setText("0 kbps"); delaySecTotal = 0; delaySecStarting = 0; delaySecStopping = 0; reconnectTimeout = 0; active = false; overloadedNotify = true; statusWidget->ui->statusIcon->setPixmap(inactivePixmap); } } void OBSBasicStatusBar::UpdateDelayMsg() { QString msg; if (delaySecTotal) { if (delaySecStarting && !delaySecStopping) { msg = QTStr("Basic.StatusBar.DelayStartingIn"); msg = msg.arg(QString::number(delaySecStarting)); } else if (!delaySecStarting && delaySecStopping) { msg = QTStr("Basic.StatusBar.DelayStoppingIn"); msg = msg.arg(QString::number(delaySecStopping)); } else if (delaySecStarting && delaySecStopping) { msg = QTStr("Basic.StatusBar.DelayStartingStoppingIn"); msg = msg.arg(QString::number(delaySecStopping), QString::number(delaySecStarting)); } else { msg = QTStr("Basic.StatusBar.Delay"); msg = msg.arg(QString::number(delaySecTotal)); } if (!statusWidget->ui->delayFrame->isVisible()) statusWidget->ui->delayFrame->show(); statusWidget->ui->delayInfo->setText(msg); } } void OBSBasicStatusBar::UpdateBandwidth() { if (!streamOutput) return; if (++seconds < bitrateUpdateSeconds) return; uint64_t bytesSent = obs_output_get_total_bytes(streamOutput); uint64_t bytesSentTime = os_gettime_ns(); if (bytesSent < lastBytesSent) bytesSent = 0; if (bytesSent == 0) lastBytesSent = 0; uint64_t bitsBetween = (bytesSent - lastBytesSent) * 8; double timePassed = double(bytesSentTime - lastBytesSentTime) / 1000000000.0; double kbitsPerSec = double(bitsBetween) / timePassed / 1000.0; QString text; text += QString::number(kbitsPerSec, 'f', 0) + QString(" kbps"); statusWidget->ui->kbps->setText(text); statusWidget->ui->kbps->setMinimumWidth( statusWidget->ui->kbps->width()); if (!statusWidget->ui->kbps->isVisible()) statusWidget->ui->kbps->show(); lastBytesSent = bytesSent; lastBytesSentTime = bytesSentTime; seconds = 0; } void OBSBasicStatusBar::UpdateCPUUsage() { OBSBasic *main = qobject_cast(parent()); if (!main) return; QString text; text += QString("CPU: ") + QString::number(main->GetCPUUsage(), 'f', 1) + QString("%"); statusWidget->ui->cpuUsage->setText(text); statusWidget->ui->cpuUsage->setMinimumWidth( statusWidget->ui->cpuUsage->width()); UpdateCurrentFPS(); } void OBSBasicStatusBar::UpdateCurrentFPS() { struct obs_video_info ovi; obs_get_video_info(&ovi); float targetFPS = (float)ovi.fps_num / (float)ovi.fps_den; QString text = QString::asprintf("%.2f / %.2f FPS", obs_get_active_fps(), targetFPS); statusWidget->ui->fpsCurrent->setText(text); statusWidget->ui->fpsCurrent->setMinimumWidth( statusWidget->ui->fpsCurrent->width()); } void OBSBasicStatusBar::UpdateStreamTime() { totalStreamSeconds++; int seconds = totalStreamSeconds % 60; int totalMinutes = totalStreamSeconds / 60; int minutes = totalMinutes % 60; int hours = totalMinutes / 60; QString text = QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); statusWidget->ui->streamTime->setText(text); if (streamOutput && !statusWidget->ui->streamTime->isEnabled()) statusWidget->ui->streamTime->setDisabled(false); if (reconnectTimeout > 0) { QString msg = QTStr("Basic.StatusBar.Reconnecting") .arg(QString::number(retries), QString::number(reconnectTimeout)); showMessage(msg); disconnected = true; statusWidget->ui->statusIcon->setPixmap(disconnectedPixmap); congestionArray.clear(); reconnectTimeout--; } else if (retries > 0) { QString msg = QTStr("Basic.StatusBar.AttemptingReconnect"); showMessage(msg.arg(QString::number(retries))); } if (delaySecStopping > 0 || delaySecStarting > 0) { if (delaySecStopping > 0) --delaySecStopping; if (delaySecStarting > 0) --delaySecStarting; UpdateDelayMsg(); } } extern volatile bool recording_paused; void OBSBasicStatusBar::UpdateRecordTime() { bool paused = os_atomic_load_bool(&recording_paused); if (!paused) { totalRecordSeconds++; int seconds = totalRecordSeconds % 60; int totalMinutes = totalRecordSeconds / 60; int minutes = totalMinutes % 60; int hours = totalMinutes / 60; QString text = QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); statusWidget->ui->recordTime->setText(text); if (recordOutput && !statusWidget->ui->recordTime->isEnabled()) statusWidget->ui->recordTime->setDisabled(false); } else { statusWidget->ui->recordIcon->setPixmap( streamPauseIconToggle ? recordingPauseInactivePixmap : recordingPausePixmap); streamPauseIconToggle = !streamPauseIconToggle; } } void OBSBasicStatusBar::UpdateDroppedFrames() { if (!streamOutput) return; int totalDropped = obs_output_get_frames_dropped(streamOutput); int totalFrames = obs_output_get_total_frames(streamOutput); double percent = (double)totalDropped / (double)totalFrames * 100.0; if (!totalFrames) return; QString text = QTStr("DroppedFrames"); text = text.arg(QString::number(totalDropped), QString::number(percent, 'f', 1)); statusWidget->ui->droppedFrames->setText(text); if (!statusWidget->ui->issuesFrame->isVisible()) statusWidget->ui->issuesFrame->show(); /* ----------------------------------- * * calculate congestion color */ float congestion = obs_output_get_congestion(streamOutput); float avgCongestion = (congestion + lastCongestion) * 0.5f; if (avgCongestion < congestion) avgCongestion = congestion; if (avgCongestion > 1.0f) avgCongestion = 1.0f; lastCongestion = congestion; if (disconnected) return; bool update = firstCongestionUpdate; float congestionOverTime = avgCongestion; if (congestionArray.size() >= congestionUpdateSeconds) { congestionOverTime = accumulate(congestionArray.begin(), congestionArray.end(), 0.0f) / (float)congestionArray.size(); congestionArray.clear(); update = true; } else { congestionArray.emplace_back(avgCongestion); } if (update) { if (congestionOverTime <= excellentThreshold + EPSILON) statusWidget->ui->statusIcon->setPixmap( excellentPixmap); else if (congestionOverTime <= goodThreshold) statusWidget->ui->statusIcon->setPixmap(goodPixmap); else if (congestionOverTime <= mediocreThreshold) statusWidget->ui->statusIcon->setPixmap(mediocrePixmap); else if (congestionOverTime <= badThreshold) statusWidget->ui->statusIcon->setPixmap(badPixmap); firstCongestionUpdate = false; } } void OBSBasicStatusBar::OBSOutputReconnect(void *data, calldata_t *params) { OBSBasicStatusBar *statusBar = reinterpret_cast(data); int seconds = (int)calldata_int(params, "timeout_sec"); QMetaObject::invokeMethod(statusBar, "Reconnect", Q_ARG(int, seconds)); } void OBSBasicStatusBar::OBSOutputReconnectSuccess(void *data, calldata_t *) { OBSBasicStatusBar *statusBar = reinterpret_cast(data); QMetaObject::invokeMethod(statusBar, "ReconnectSuccess"); } void OBSBasicStatusBar::Reconnect(int seconds) { OBSBasic *main = qobject_cast(parent()); if (!retries) main->SysTrayNotify( QTStr("Basic.SystemTray.Message.Reconnecting"), QSystemTrayIcon::Warning); reconnectTimeout = seconds; if (streamOutput) { delaySecTotal = obs_output_get_active_delay(streamOutput); UpdateDelayMsg(); retries++; } } void OBSBasicStatusBar::ReconnectClear() { retries = 0; reconnectTimeout = 0; seconds = -1; lastBytesSent = 0; lastBytesSentTime = os_gettime_ns(); delaySecTotal = 0; UpdateDelayMsg(); } void OBSBasicStatusBar::ReconnectSuccess() { OBSBasic *main = qobject_cast(parent()); QString msg = QTStr("Basic.StatusBar.ReconnectSuccessful"); showMessage(msg, 4000); main->SysTrayNotify(msg, QSystemTrayIcon::Information); ReconnectClear(); if (streamOutput) { delaySecTotal = obs_output_get_active_delay(streamOutput); UpdateDelayMsg(); disconnected = false; firstCongestionUpdate = true; } } void OBSBasicStatusBar::UpdateStatusBar() { OBSBasic *main = qobject_cast(parent()); UpdateBandwidth(); if (streamOutput) UpdateStreamTime(); if (recordOutput) UpdateRecordTime(); UpdateDroppedFrames(); int skipped = video_output_get_skipped_frames(obs_get_video()); int total = video_output_get_total_frames(obs_get_video()); skipped -= startSkippedFrameCount; total -= startTotalFrameCount; int diff = skipped - lastSkippedFrameCount; double percentage = double(skipped) / double(total) * 100.0; if (diff > 10 && percentage >= 0.1f) { showMessage(QTStr("HighResourceUsage"), 4000); if (!main->isVisible() && overloadedNotify) { main->SysTrayNotify(QTStr("HighResourceUsage"), QSystemTrayIcon::Warning); overloadedNotify = false; } } lastSkippedFrameCount = skipped; } void OBSBasicStatusBar::StreamDelayStarting(int sec) { OBSBasic *main = qobject_cast(parent()); if (!main || !main->outputHandler) return; streamOutput = main->outputHandler->streamOutput; delaySecTotal = delaySecStarting = sec; UpdateDelayMsg(); Activate(); } void OBSBasicStatusBar::StreamDelayStopping(int sec) { delaySecTotal = delaySecStopping = sec; UpdateDelayMsg(); } void OBSBasicStatusBar::StreamStarted(obs_output_t *output) { streamOutput = output; signal_handler_connect(obs_output_get_signal_handler(streamOutput), "reconnect", OBSOutputReconnect, this); signal_handler_connect(obs_output_get_signal_handler(streamOutput), "reconnect_success", OBSOutputReconnectSuccess, this); retries = 0; lastBytesSent = 0; lastBytesSentTime = os_gettime_ns(); Activate(); } void OBSBasicStatusBar::StreamStopped() { if (streamOutput) { signal_handler_disconnect( obs_output_get_signal_handler(streamOutput), "reconnect", OBSOutputReconnect, this); signal_handler_disconnect( obs_output_get_signal_handler(streamOutput), "reconnect_success", OBSOutputReconnectSuccess, this); ReconnectClear(); streamOutput = nullptr; clearMessage(); Deactivate(); } } void OBSBasicStatusBar::RecordingStarted(obs_output_t *output) { recordOutput = output; Activate(); } void OBSBasicStatusBar::RecordingStopped() { recordOutput = nullptr; Deactivate(); } void OBSBasicStatusBar::RecordingPaused() { QString text = statusWidget->ui->recordTime->text() + QStringLiteral(" (PAUSED)"); statusWidget->ui->recordTime->setText(text); if (recordOutput) { statusWidget->ui->recordIcon->setPixmap(recordingPausePixmap); streamPauseIconToggle = true; } } void OBSBasicStatusBar::RecordingUnpaused() { if (recordOutput) { statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap); } } static QPixmap GetPixmap(const QString &filename) { bool darkTheme = obs_frontend_is_theme_dark(); QString path; if (darkTheme) { std::string darkPath; QString themePath = QString("themes/Dark/") + filename; GetDataFilePath(QT_TO_UTF8(themePath), darkPath); path = QT_UTF8(darkPath.c_str()); } else { path = QString(":/res/images/" + filename); } return QIcon(path).pixmap(QSize(16, 16)); } void OBSBasicStatusBar::UpdateIcons() { disconnectedPixmap = GetPixmap("network-disconnected.svg"); inactivePixmap = GetPixmap("network-inactive.svg"); streamingInactivePixmap = GetPixmap("streaming-inactive.svg"); recordingInactivePixmap = GetPixmap("recording-inactive.svg"); recordingPauseInactivePixmap = GetPixmap("recording-pause-inactive.svg"); bool streaming = obs_frontend_streaming_active(); if (!streaming) { statusWidget->ui->streamIcon->setPixmap( streamingInactivePixmap); statusWidget->ui->statusIcon->setPixmap(inactivePixmap); } else { if (disconnected) statusWidget->ui->statusIcon->setPixmap( disconnectedPixmap); } bool recording = obs_frontend_recording_active(); if (!recording) statusWidget->ui->recordIcon->setPixmap( recordingInactivePixmap); }