Files
obs-studio/UI/window-basic-status-bar.cpp
Aleks Todorov bad7b78fe4 UI: Fix text stacking in paused indicator
Currently, the paused indicator is never undone, instead relying on the
update timer to eventually erase it away when the recording duration is
updated. If the user spams pause fast enough, the indicator will stack
several times before it is erased - especially if the unpaused branch in
the update timer never has a chance to run.

Instead of mutating the recordTime text on pause and requiring an undo,
extract the UI update to a separate function which computes the full
text based on the current state. Call this function when pause is
toggled, thereby forcing an accurate UI update that either does or does
not incude the paused text.

An added benefit is that the paused indicator now disappears
immediately.
2024-05-04 13:39:34 -07:00

627 lines
16 KiB
C++

#include <QPainter>
#include <QPixmap>
#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)))
{
congestionArray.reserve(congestionUpdateSeconds);
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, 1);
setMinimumHeight(statusWidget->height());
UpdateIcons();
connect(App(), &OBSApp::StyleChanged, this,
&OBSBasicStatusBar::UpdateIcons);
messageTimer = new QTimer(this);
messageTimer->setSingleShot(true);
connect(messageTimer, &QTimer::timeout, this,
&OBSBasicStatusBar::clearMessage);
clearMessage();
}
void OBSBasicStatusBar::Activate()
{
if (!active) {
refreshTimer = new QTimer(this);
connect(refreshTimer, &QTimer::timeout, this,
&OBSBasicStatusBar::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<OBSBasic *>(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<OBSBasic *>(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++;
if (recordOutput && !statusWidget->ui->recordTime->isEnabled())
statusWidget->ui->recordTime->setDisabled(false);
} else {
statusWidget->ui->recordIcon->setPixmap(
streamPauseIconToggle ? recordingPauseInactivePixmap
: recordingPausePixmap);
streamPauseIconToggle = !streamPauseIconToggle;
}
UpdateRecordTimeLabel();
}
void OBSBasicStatusBar::UpdateRecordTimeLabel()
{
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);
if (os_atomic_load_bool(&recording_paused)) {
text += QStringLiteral(" (PAUSED)");
}
statusWidget->ui->recordTime->setText(text);
}
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<OBSBasicStatusBar *>(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<OBSBasicStatusBar *>(data);
QMetaObject::invokeMethod(statusBar, "ReconnectSuccess");
}
void OBSBasicStatusBar::Reconnect(int seconds)
{
OBSBasic *main = qobject_cast<OBSBasic *>(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<OBSBasic *>(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<OBSBasic *>(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<OBSBasic *>(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;
streamSigs.emplace_back(obs_output_get_signal_handler(streamOutput),
"reconnect", OBSOutputReconnect, this);
streamSigs.emplace_back(obs_output_get_signal_handler(streamOutput),
"reconnect_success", OBSOutputReconnectSuccess,
this);
retries = 0;
lastBytesSent = 0;
lastBytesSentTime = os_gettime_ns();
Activate();
}
void OBSBasicStatusBar::StreamStopped()
{
if (streamOutput) {
streamSigs.clear();
ReconnectClear();
streamOutput = nullptr;
clearMessage();
Deactivate();
}
}
void OBSBasicStatusBar::RecordingStarted(obs_output_t *output)
{
recordOutput = output;
Activate();
}
void OBSBasicStatusBar::RecordingStopped()
{
recordOutput = nullptr;
Deactivate();
}
void OBSBasicStatusBar::RecordingPaused()
{
if (recordOutput) {
statusWidget->ui->recordIcon->setPixmap(recordingPausePixmap);
streamPauseIconToggle = true;
}
UpdateRecordTimeLabel();
}
void OBSBasicStatusBar::RecordingUnpaused()
{
if (recordOutput) {
statusWidget->ui->recordIcon->setPixmap(recordingActivePixmap);
}
UpdateRecordTimeLabel();
}
static QPixmap GetPixmap(const QString &filename)
{
QString path = obs_frontend_is_theme_dark() ? "theme:Dark/"
: ":/res/images/";
return QIcon(path + filename).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);
}
void OBSBasicStatusBar::showMessage(const QString &message, int timeout)
{
messageTimer->stop();
statusWidget->ui->message->setText(message);
if (timeout)
messageTimer->start(timeout);
}
void OBSBasicStatusBar::clearMessage()
{
statusWidget->ui->message->setText("");
}