0
0
mirror of https://github.com/obsproject/obs-studio.git synced 2024-09-20 04:42:18 +02:00
obs-studio/UI/window-basic-status-bar.cpp
cg2121 b19f922747 UI: Fix alignment of status bar message
The status bar message was not vertically aligned properly to
other widgets in the status bar. We have to implement our own
message system here, as the default status bar in Qt has hardcoded
paddings.
2023-08-21 17:46:25 -03:00

631 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)))
{
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++;
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<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;
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);
}
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("");
}