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