diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 55f2e65a155a5e760a4d07622be1625f4e7c9f8c..1293514973a9a6a33582630327a646060bd1c157 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -131,6 +131,7 @@ set(obs_SOURCES obs-app.cpp api-interface.cpp window-basic-main.cpp + window-basic-stats.cpp window-basic-filters.cpp window-basic-settings.cpp window-basic-interaction.cpp @@ -179,6 +180,7 @@ set(obs_HEADERS platform.hpp window-main.hpp window-basic-main.hpp + window-basic-stats.hpp window-basic-filters.hpp window-basic-settings.hpp window-basic-interaction.hpp diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index f41ec5d46b59054b4f57eb5ea0d91000e2319413..66075f07dcf1a55e205202a56f893866886bff41 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -131,6 +131,25 @@ Basic.AutoConfig.TestPage.Result.RecordingEncoder="Recording Encoder" Basic.AutoConfig.TestPage.Result.Header="The program has determined that these estimated settings are the most ideal for you:" Basic.AutoConfig.TestPage.Result.Footer="To use these settings, click Apply Settings. To reconfigure the wizard and try again, click Back. To manually configure settings yourself, click Cancel and open Settings." +# stats +Basic.Stats="Stats" +Basic.Stats.CPUUsage="CPU Usage" +Basic.Stats.HDDSpaceAvailable="HDD space available" +Basic.Stats.MemoryUsage="Memory Usage" +Basic.Stats.AverageTimeToRender="Average time to render frame" +Basic.Stats.SkipppedFrames="Skipped frames due to encoding lag" +Basic.Stats.MissedFrames="Frames missed due to rendering lag" +Basic.Stats.Output.Stream="Stream" +Basic.Stats.Output.Recording="Recording" +Basic.Stats.Status="Status" +Basic.Stats.Status.Recording="Recording" +Basic.Stats.Status.Live="LIVE" +Basic.Stats.Status.Reconnecting="Reconnecting" +Basic.Stats.Status.Inactive="Inactive" +Basic.Stats.DroppedFrames="Dropped Frames (Network)" +Basic.Stats.MegabytesSent="Total Data Output" +Basic.Stats.Bitrate="Bitrate" + # updater Updater.Title="New update available" Updater.Text="There is a new update available:" diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui index 0e284964e29f176746f0282ba1f205f3753bdebc..bf0c1a1214cc8505d1b069a739e7120da1624efa 100644 --- a/UI/forms/OBSBasic.ui +++ b/UI/forms/OBSBasic.ui @@ -987,6 +987,7 @@ Basic.MainMenu.Tools + @@ -1471,6 +1472,11 @@ Basic.AutoConfig.Beta + + + Basic.Stats + + diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 1229d1e4865cf7a2473f6a092e5ba0de9b50680d..5e86c2c8b60aff3a7d68991b8c9998f40850dea5 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -43,6 +43,7 @@ #include "window-basic-auto-config.hpp" #include "window-basic-source-select.hpp" #include "window-basic-main.hpp" +#include "window-basic-stats.hpp" #include "window-basic-main-outputs.hpp" #include "window-basic-properties.hpp" #include "window-log-reply.hpp" @@ -2866,6 +2867,8 @@ void OBSBasic::CloseDialogs() delete projector; projector.clear(); } + + delete stats; } void OBSBasic::EnumDialogs() @@ -5486,3 +5489,13 @@ void OBSBasic::on_autoConfigure_triggered() test.show(); test.exec(); } + +void OBSBasic::on_stats_triggered() +{ + stats.clear(); + OBSBasicStats *statsDlg; + statsDlg = new OBSBasicStats(nullptr); + statsDlg->setModal(false); + statsDlg->show(); + stats = statsDlg; +} diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 80805652953083948d5445880de21401e5e6111b..98adda36e0ba94149a821fba658242e2cd56f789 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -42,6 +42,7 @@ class QMessageBox; class QListWidgetItem; class VolControl; class QNetworkReply; +class OBSBasicStats; #include "ui_OBSBasic.h" @@ -160,6 +161,8 @@ private: QPointer projectors[10]; QList> windowProjectors; + QPointer stats; + QPointer startStreamMenu; QPointer replayBufferButton; @@ -614,6 +617,7 @@ private slots: void on_modeSwitch_clicked(); void on_autoConfigure_triggered(); + void on_stats_triggered(); void logUploadFinished(const QString &text, const QString &error); diff --git a/UI/window-basic-stats.cpp b/UI/window-basic-stats.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2b849f7e11434ddeb3debc07ced08785ee29dd18 --- /dev/null +++ b/UI/window-basic-stats.cpp @@ -0,0 +1,455 @@ +#include "obs-frontend-api/obs-frontend-api.h" + +#include "window-basic-stats.hpp" +#include "window-basic-main.hpp" +#include "platform.hpp" +#include "obs-app.hpp" + +#include +#include +#include +#include +#include + +#include + +#define TIMER_INTERVAL 2000 + +static void setThemeID(QWidget *widget, const QString &themeID) +{ + if (widget->property("themeID").toString() != themeID) { + widget->setProperty("themeID", themeID); + + /* force style sheet recalculation */ + QString qss = widget->styleSheet(); + widget->setStyleSheet("/* */"); + widget->setStyleSheet(qss); + } +} + +OBSBasicStats::OBSBasicStats(QWidget *parent) + : QDialog (parent), + cpu_info (os_cpu_usage_info_start()), + timer (this) +{ + QVBoxLayout *mainLayout = new QVBoxLayout(); + QGridLayout *topLayout = new QGridLayout(); + outputLayout = new QGridLayout(); + + int row = 0; + + auto newStatBare = [&] (QString name, QWidget *label, int col) + { + QLabel *typeLabel = new QLabel(name, this); + topLayout->addWidget(typeLabel, row, col); + topLayout->addWidget(label, row++, col + 1); + }; + + auto newStat = [&] (const char *strLoc, QWidget *label, int col) + { + std::string str = "Basic.Stats."; + str += strLoc; + newStatBare(QTStr(str.c_str()), label, col); + }; + + /* --------------------------------------------- */ + + cpuUsage = new QLabel(this); + hddSpace = new QLabel(this); +#ifdef _WIN32 + memUsage = new QLabel(this); +#endif + + newStat("CPUUsage", cpuUsage, 0); + newStat("HDDSpaceAvailable", hddSpace, 0); +#ifdef _WIN32 + newStat("MemoryUsage", memUsage, 0); +#endif + + fps = new QLabel(this); + renderTime = new QLabel(this); + skippedFrames = new QLabel(this); + missedFrames = new QLabel(this); + row = 0; + + newStatBare("FPS", fps, 2); + newStat("AverageTimeToRender", renderTime, 2); + newStat("SkipppedFrames", skippedFrames, 2); + newStat("MissedFrames", missedFrames, 2); + + /* --------------------------------------------- */ + + QPushButton *closeButton = new QPushButton(QTStr("Close")); + QPushButton *resetButton = new QPushButton(QTStr("Reset")); + QHBoxLayout *buttonLayout = new QHBoxLayout; + buttonLayout->addStretch(); + buttonLayout->addWidget(resetButton); + buttonLayout->addWidget(closeButton); + + /* --------------------------------------------- */ + + int col = 0; + auto addOutputCol = [&] (const char *loc) + { + QLabel *label = new QLabel(QTStr(loc), this); + label->setStyleSheet("font-weight: bold"); + outputLayout->addWidget(label, 0, col++); + }; + + addOutputCol("Basic.Settings.Output"); + addOutputCol("Basic.Stats.Status"); + addOutputCol("Basic.Stats.DroppedFrames"); + addOutputCol("Basic.Stats.MegabytesSent"); + addOutputCol("Basic.Stats.Bitrate"); + + /* --------------------------------------------- */ + + AddOutputLabels(QTStr("Basic.Stats.Output.Stream")); + AddOutputLabels(QTStr("Basic.Stats.Output.Recording")); + + /* --------------------------------------------- */ + + QVBoxLayout *outputContainerLayout = new QVBoxLayout(); + outputContainerLayout->addLayout(outputLayout); + outputContainerLayout->addStretch(); + + QWidget *widget = new QWidget(this); + widget->setLayout(outputContainerLayout); + + QScrollArea *scrollArea = new QScrollArea(this); + scrollArea->setWidget(widget); + scrollArea->setWidgetResizable(true); + + /* --------------------------------------------- */ + + mainLayout->addLayout(topLayout); + mainLayout->addWidget(scrollArea); + mainLayout->addLayout(buttonLayout); + setLayout(mainLayout); + + /* --------------------------------------------- */ + + connect(closeButton, &QPushButton::clicked, [this] () {close();}); + connect(resetButton, &QPushButton::clicked, [this] () {Reset();}); + + installEventFilter(CreateShortcutFilter()); + + resize(800, 280); + setWindowTitle(QTStr("Basic.Stats")); + setSizeGripEnabled(true); + setWindowModality(Qt::NonModal); + setAttribute(Qt::WA_DeleteOnClose, true); + + QObject::connect(&timer, &QTimer::timeout, this, &OBSBasicStats::Update); + timer.setInterval(TIMER_INTERVAL); + timer.start(); + Update(); +} + +OBSBasicStats::~OBSBasicStats() +{ + os_cpu_usage_info_destroy(cpu_info); +} + +void OBSBasicStats::AddOutputLabels(QString name) +{ + OutputLabels ol; + ol.name = new QLabel(name, this); + ol.status = new QLabel(this); + ol.droppedFrames = new QLabel(this); + ol.megabytesSent = new QLabel(this); + ol.bitrate = new QLabel(this); + + int newPointSize = ol.status->font().pointSize(); + newPointSize *= 13; + newPointSize /= 10; + QString qss = + QString("font-size: %1pt").arg(QString::number(newPointSize)); + ol.status->setStyleSheet(qss); + + int col = 0; + int row = outputLabels.size() + 1; + outputLayout->addWidget(ol.name, row, col++); + outputLayout->addWidget(ol.status, row, col++); + outputLayout->addWidget(ol.droppedFrames, row, col++); + outputLayout->addWidget(ol.megabytesSent, row, col++); + outputLayout->addWidget(ol.bitrate, row, col++); + outputLabels.push_back(ol); +} + +static uint32_t first_encoded = 0xFFFFFFFF; +static uint32_t first_skipped = 0xFFFFFFFF; +static uint32_t first_rendered = 0xFFFFFFFF; +static uint32_t first_lagged = 0xFFFFFFFF; + +void OBSBasicStats::Update() +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + /* TODO: Un-hardcode */ + + struct obs_video_info ovi = {}; + obs_get_video_info(&ovi); + + OBSOutput strOutput = obs_frontend_get_streaming_output(); + OBSOutput recOutput = obs_frontend_get_recording_output(); + obs_output_release(strOutput); + obs_output_release(recOutput); + + /* ------------------------------------------- */ + /* general usage */ + + double curFPS = obs_get_active_fps(); + double obsFPS = (double)ovi.fps_num / (double)ovi.fps_den; + + QString str = QString::number(curFPS, 'f', 2); + fps->setText(str); + + if (curFPS < (obsFPS * 0.8)) + setThemeID(fps, "error"); + else if (curFPS < (obsFPS * 0.95)) + setThemeID(fps, "warning"); + else + setThemeID(fps, ""); + + /* ------------------ */ + + double usage = os_cpu_usage_info_query(cpu_info); + str = QString::number(usage, 'g', 2) + QStringLiteral("%"); + cpuUsage->setText(str); + + /* ------------------ */ + + const char *mode = config_get_string(main->Config(), "Output", "Mode"); + const char *path = strcmp(mode, "Advanced") ? + config_get_string(main->Config(), "SimpleOutput", "FilePath") : + config_get_string(main->Config(), "AdvOut", "RecFilePath"); + +#define MBYTE (1024ULL * 1024ULL) +#define GBYTE (1024ULL * 1024ULL * 1024ULL) +#define TBYTE (1024ULL * 1024ULL * 1024ULL * 1024ULL) + uint64_t num_bytes = os_get_free_disk_space(path); + QString abrv = QStringLiteral(" MB"); + long double num; + + num = (long double)num_bytes / (1024.0l * 1024.0l); + if (num_bytes > TBYTE) { + num /= 1024.0l * 1024.0l; + abrv = QStringLiteral(" TB"); + } else if (num_bytes > GBYTE) { + num /= 1024.0l; + abrv = QStringLiteral(" GB"); + } + + str = QString::number(num, 'f', 1) + abrv; + hddSpace->setText(str); + + if (num_bytes < GBYTE) + setThemeID(hddSpace, "error"); + else if (num_bytes < (5 * GBYTE)) + setThemeID(hddSpace, "warning"); + else + setThemeID(hddSpace, ""); + + /* ------------------ */ + +#ifdef _WIN32 + num = (long double)CurrentMemoryUsage() / (1024.0l * 1024.0l); + + str = QString::number(num, 'f', 1) + QStringLiteral(" MB"); + memUsage->setText(str); +#endif + + /* ------------------ */ + + num = (long double)obs_get_average_frame_time_ns() / 1000000.0l; + + str = QString::number(num, 'f', 1) + QStringLiteral(" ms"); + renderTime->setText(str); + + long double fpsFrameTime = + (long double)ovi.fps_den * 1000.0l / (long double)ovi.fps_num; + + if (num > fpsFrameTime) + setThemeID(renderTime, "error"); + else if (num > fpsFrameTime * 0.75l) + setThemeID(renderTime, "warning"); + else + setThemeID(renderTime, ""); + + /* ------------------ */ + + video_t *video = obs_get_video(); + uint32_t total_encoded = video_output_get_total_frames(video); + uint32_t total_skipped = video_output_get_skipped_frames(video); + + if (total_encoded < first_encoded || total_skipped < first_skipped) { + first_encoded = total_encoded; + first_skipped = total_skipped; + } + total_encoded -= first_encoded; + total_skipped -= first_skipped; + + num = total_encoded + ? (long double)total_skipped / (long double)total_encoded + : 0.0l; + num *= 100.0l; + + str = QString("%1 / %2 (%3%)").arg( + QString::number(total_skipped), + QString::number(total_encoded), + QString::number(num, 'f', 1)); + skippedFrames->setText(str); + + if (num > 5.0l) + setThemeID(skippedFrames, "error"); + else if (num > 1.0l) + setThemeID(skippedFrames, "warning"); + else + setThemeID(skippedFrames, ""); + + /* ------------------ */ + + uint32_t total_rendered = obs_get_total_frames(); + uint32_t total_lagged = obs_get_lagged_frames(); + + if (total_rendered < first_rendered || total_lagged < first_lagged) { + first_rendered = total_rendered; + first_lagged = total_lagged; + } + total_rendered -= first_rendered; + total_lagged -= first_lagged; + + num = total_rendered + ? (long double)total_lagged / (long double)total_rendered + : 0.0l; + num *= 100.0l; + + str = QString("%1 / %2 (%3%)").arg( + QString::number(total_lagged), + QString::number(total_rendered), + QString::number(num, 'f', 1)); + missedFrames->setText(str); + + if (num > 5.0l) + setThemeID(missedFrames, "error"); + else if (num > 1.0l) + setThemeID(missedFrames, "warning"); + else + setThemeID(missedFrames, ""); + + /* ------------------------------------------- */ + /* recording/streaming stats */ + + outputLabels[0].Update(strOutput); + outputLabels[1].Update(recOutput); +} + +void OBSBasicStats::Reset() +{ + timer.start(); + + first_encoded = 0xFFFFFFFF; + first_skipped = 0xFFFFFFFF; + first_rendered = 0xFFFFFFFF; + first_lagged = 0xFFFFFFFF; + + OBSOutput strOutput = obs_frontend_get_streaming_output(); + OBSOutput recOutput = obs_frontend_get_recording_output(); + obs_output_release(strOutput); + obs_output_release(recOutput); + + outputLabels[0].Reset(strOutput); + outputLabels[1].Reset(recOutput); + Update(); +} + +void OBSBasicStats::OutputLabels::Update(obs_output_t *output) +{ + const char *id = obs_obj_get_id(output); + bool rec = strcmp(id, "rtmp_output") != 0; + + uint64_t totalBytes = obs_output_get_total_bytes(output); + uint64_t curTime = os_gettime_ns(); + uint64_t bytesSent = totalBytes; + + if (bytesSent < lastBytesSent) + bytesSent = 0; + if (bytesSent == 0) + lastBytesSent = 0; + + uint64_t bitsBetween = (bytesSent - lastBytesSent) * 8; + long double timePassed = (long double)(curTime - lastBytesSentTime) / + 1000000000.0l; + long double kbps = (long double)bitsBetween / + timePassed / 1000.0l; + + if (timePassed < 0.01l) + kbps = 0.0l; + + QString str = QTStr("Basic.Stats.Status.Inactive"); + QString themeID; + if (rec) { + if (obs_output_active(output)) + str = QTStr("Basic.Stats.Status.Recording"); + } else { + if (obs_output_active(output)) { + if (obs_output_reconnecting(output)) { + str = QTStr("Basic.Stats.Status.Reconnecting"); + themeID = "error"; + } else { + str = QTStr("Basic.Stats.Status.Live"); + themeID = "good"; + } + } + } + + status->setText(str); + setThemeID(status, themeID); + + long double num = (long double)totalBytes / (1024.0l * 1024.0l); + + megabytesSent->setText( + QString("%1 MB").arg(QString::number(num, 'f', 1))); + bitrate->setText( + QString("%1 kb/s").arg(QString::number(kbps, 'f', 0))); + + if (!rec) { + int total = obs_output_get_total_frames(output); + int dropped = obs_output_get_frames_dropped(output); + + if (total < first_total || dropped < first_dropped) { + first_total = 0; + first_dropped = 0; + } + + total -= first_total; + dropped -= first_dropped; + + num = total + ? (long double)dropped / (long double)total * 100.0l + : 0.0l; + + str = QString("%1 / %2 (%3%)").arg( + QString::number(dropped), + QString::number(total), + QString::number(num, 'f', 1)); + droppedFrames->setText(str); + + if (num > 5.0l) + setThemeID(droppedFrames, "error"); + else if (num > 1.0l) + setThemeID(droppedFrames, "warning"); + else + setThemeID(droppedFrames, ""); + } + + lastBytesSent = bytesSent; + lastBytesSentTime = curTime; +} + +void OBSBasicStats::OutputLabels::Reset(obs_output_t *output) +{ + first_total = obs_output_get_total_frames(output); + first_dropped = obs_output_get_frames_dropped(output); +} diff --git a/UI/window-basic-stats.hpp b/UI/window-basic-stats.hpp new file mode 100644 index 0000000000000000000000000000000000000000..f84a90ac2a5d2feacfa23a14d46ca4efaa71f3bd --- /dev/null +++ b/UI/window-basic-stats.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class QGridLayout; + +class OBSBasicStats : public QDialog { + Q_OBJECT + + QLabel *fps = nullptr; + QLabel *cpuUsage = nullptr; + QLabel *hddSpace = nullptr; + QLabel *memUsage = nullptr; + + QLabel *renderTime = nullptr; + QLabel *skippedFrames = nullptr; + QLabel *missedFrames = nullptr; + + QGridLayout *outputLayout = nullptr; + + os_cpu_usage_info_t *cpu_info = nullptr; + + QTimer timer; + + struct OutputLabels { + QPointer name; + QPointer status; + QPointer droppedFrames; + QPointer megabytesSent; + QPointer bitrate; + + uint64_t lastBytesSent = 0; + uint64_t lastBytesSentTime = 0; + + int first_total = 0; + int first_dropped = 0; + + void Update(obs_output_t *output); + void Reset(obs_output_t *output); + }; + + QList outputLabels; + + void AddOutputLabels(QString name); + void Update(); + void Reset(); + +public: + OBSBasicStats(QWidget *parent = nullptr); + ~OBSBasicStats(); +};