10be82cba5
Made-with: Cursor
348 lines
8.6 KiB
C++
348 lines
8.6 KiB
C++
#include <QDockWidget>
|
|
#include <QMainWindow>
|
|
#include <QWidget>
|
|
#include <QLabel>
|
|
#include <QGridLayout>
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QFrame>
|
|
#include <QTimer>
|
|
#include <QFont>
|
|
#include <QPalette>
|
|
|
|
#include <obs-frontend-api.h>
|
|
#include <util/platform.h>
|
|
|
|
#include "stats-dialog.hpp"
|
|
|
|
extern "C" {
|
|
#include "irl-source.h"
|
|
}
|
|
|
|
/* ---- helpers ---- */
|
|
|
|
static QString fmt_bytes(uint64_t b)
|
|
{
|
|
if (b < 1024)
|
|
return QString("%1 B").arg(b);
|
|
if (b < 1024ULL * 1024)
|
|
return QString("%1 KB").arg(b / 1024.0, 0, 'f', 1);
|
|
if (b < 1024ULL * 1024 * 1024)
|
|
return QString("%1 MB").arg(b / (1024.0 * 1024.0), 0, 'f', 1);
|
|
return QString("%1 GB").arg(b / (1024.0 * 1024.0 * 1024.0), 0, 'f',
|
|
2);
|
|
}
|
|
|
|
static QString fmt_bitrate(int64_t kbps)
|
|
{
|
|
if (kbps <= 0)
|
|
return "-";
|
|
if (kbps < 1000)
|
|
return QString("%1 kbps").arg(kbps);
|
|
return QString("%1 Mbps").arg(kbps / 1000.0, 0, 'f', 1);
|
|
}
|
|
|
|
static QString fmt_uptime(uint64_t start_ns)
|
|
{
|
|
if (!start_ns)
|
|
return "-";
|
|
uint64_t now = os_gettime_ns();
|
|
uint64_t sec = (now > start_ns) ? (now - start_ns) / 1000000000ULL : 0;
|
|
int h = (int)(sec / 3600);
|
|
int m = (int)((sec % 3600) / 60);
|
|
int s = (int)(sec % 60);
|
|
return QString("%1:%2:%3")
|
|
.arg(h, 2, 10, QChar('0'))
|
|
.arg(m, 2, 10, QChar('0'))
|
|
.arg(s, 2, 10, QChar('0'));
|
|
}
|
|
|
|
/* ---- widget ---- */
|
|
|
|
static const char *status_colors[] = {"#888888", "#e0a020", "#20c040",
|
|
"#e04040"};
|
|
|
|
class StreamStatsWidget : public QWidget {
|
|
public:
|
|
StreamStatsWidget(bool is_de, QWidget *parent = nullptr)
|
|
: QWidget(parent),
|
|
m_de(is_de)
|
|
{
|
|
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
|
|
buildUI();
|
|
|
|
m_timer = new QTimer(this);
|
|
QObject::connect(m_timer, &QTimer::timeout,
|
|
[this]() { refresh(); });
|
|
m_timer->start(500);
|
|
refresh();
|
|
}
|
|
|
|
private:
|
|
bool m_de;
|
|
QLabel *m_dot, *m_status;
|
|
QLabel *m_lbls[4], *m_vals[4];
|
|
QLabel *m_videoLine, *m_audioLine, *m_serverLine;
|
|
QTimer *m_timer;
|
|
int64_t m_prevFrames = 0;
|
|
uint64_t m_prevTime = 0;
|
|
double m_fps = 0.0;
|
|
|
|
QLabel *makeLabel(const QString &text, int ptDelta, bool bold,
|
|
const QString &color)
|
|
{
|
|
auto *l = new QLabel(text, this);
|
|
QFont f = font();
|
|
f.setPointSize(f.pointSize() + ptDelta);
|
|
f.setBold(bold);
|
|
l->setFont(f);
|
|
if (!color.isEmpty())
|
|
l->setStyleSheet(
|
|
QString("QLabel{color:%1}").arg(color));
|
|
l->setTextInteractionFlags(Qt::NoTextInteraction);
|
|
return l;
|
|
}
|
|
|
|
void buildUI()
|
|
{
|
|
QString dim = palette()
|
|
.color(QPalette::PlaceholderText)
|
|
.name();
|
|
QString acc = palette().color(QPalette::Highlight).name();
|
|
|
|
auto *root = new QVBoxLayout(this);
|
|
root->setContentsMargins(12, 10, 12, 10);
|
|
root->setSpacing(8);
|
|
|
|
/* Row 1 — status */
|
|
auto *row1 = new QHBoxLayout();
|
|
row1->setSpacing(8);
|
|
|
|
m_dot = new QLabel(this);
|
|
m_dot->setFixedSize(10, 10);
|
|
m_dot->setStyleSheet(
|
|
"QLabel{background:#888;border-radius:5px;"
|
|
"min-width:10px;min-height:10px}");
|
|
row1->addWidget(m_dot, 0, Qt::AlignVCenter);
|
|
|
|
m_status = makeLabel(m_de ? "Inaktiv" : "Idle", 1, true, "");
|
|
row1->addWidget(m_status, 0, Qt::AlignVCenter);
|
|
row1->addStretch();
|
|
root->addLayout(row1);
|
|
|
|
/* Row 2 — stats grid */
|
|
auto *grid = new QGridLayout();
|
|
grid->setHorizontalSpacing(12);
|
|
grid->setVerticalSpacing(2);
|
|
for (int c = 0; c < 4; c++)
|
|
grid->setColumnStretch(c, 1);
|
|
|
|
QFont lblFont = font();
|
|
lblFont.setPointSize(lblFont.pointSize() - 2);
|
|
lblFont.setBold(true);
|
|
|
|
QFont valFont("Consolas", font().pointSize() + 2);
|
|
valFont.setBold(true);
|
|
|
|
QString headers[4] = {"BITRATE", "FPS",
|
|
m_de ? "UPTIME" : "UPTIME",
|
|
m_de ? "DATEN" : "DATA"};
|
|
|
|
for (int c = 0; c < 4; c++) {
|
|
m_lbls[c] = new QLabel(headers[c], this);
|
|
m_lbls[c]->setFont(lblFont);
|
|
m_lbls[c]->setStyleSheet(
|
|
QString("QLabel{color:%1}").arg(dim));
|
|
m_lbls[c]->setTextInteractionFlags(
|
|
Qt::NoTextInteraction);
|
|
m_lbls[c]->setAlignment(Qt::AlignLeft |
|
|
Qt::AlignBottom);
|
|
grid->addWidget(m_lbls[c], 0, c);
|
|
|
|
m_vals[c] = new QLabel("-", this);
|
|
m_vals[c]->setFont(valFont);
|
|
m_vals[c]->setStyleSheet(
|
|
QString("QLabel{color:%1}").arg(acc));
|
|
m_vals[c]->setTextInteractionFlags(
|
|
Qt::NoTextInteraction);
|
|
m_vals[c]->setAlignment(Qt::AlignLeft |
|
|
Qt::AlignTop);
|
|
grid->addWidget(m_vals[c], 1, c);
|
|
}
|
|
root->addLayout(grid);
|
|
|
|
/* Separator */
|
|
auto *sep = new QFrame(this);
|
|
sep->setFrameShape(QFrame::HLine);
|
|
sep->setFrameShadow(QFrame::Sunken);
|
|
root->addWidget(sep);
|
|
|
|
/* Info lines */
|
|
m_videoLine = makeLabel("-", -1, false, "");
|
|
m_audioLine = makeLabel("-", -1, false, "");
|
|
m_serverLine = makeLabel("-", -1, false, dim);
|
|
root->addWidget(m_videoLine);
|
|
root->addWidget(m_audioLine);
|
|
root->addWidget(m_serverLine);
|
|
|
|
root->addStretch();
|
|
}
|
|
|
|
void refresh()
|
|
{
|
|
struct irl_source_data *d = nullptr;
|
|
for (int i = 0; i < g_irl_source_count && i < MAX_IRL_SOURCES;
|
|
i++) {
|
|
if (g_irl_sources[i]) {
|
|
d = g_irl_sources[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!d) {
|
|
setNoSource();
|
|
return;
|
|
}
|
|
|
|
long state = os_atomic_load_long(&d->connection_state);
|
|
if (state < 0 || state > 3)
|
|
state = 0;
|
|
bool conn = (state == CONN_STATE_CONNECTED);
|
|
|
|
static const char *de[] = {"Inaktiv", "Wartet\xe2\x80\xa6",
|
|
"Verbunden", "Getrennt"};
|
|
static const char *en[] = {"Idle", "Listening\xe2\x80\xa6",
|
|
"Connected", "Disconnected"};
|
|
|
|
m_dot->setStyleSheet(
|
|
QString("QLabel{background:%1;border-radius:5px;"
|
|
"min-width:10px;min-height:10px}")
|
|
.arg(status_colors[state]));
|
|
m_status->setText(m_de ? de[state] : en[state]);
|
|
|
|
/* Bitrate */
|
|
m_vals[0]->setText(
|
|
conn ? fmt_bitrate(d->current_bitrate_kbps) : "-");
|
|
|
|
/* FPS */
|
|
uint64_t now = os_gettime_ns();
|
|
int64_t frames = d->stats_total_frames;
|
|
if (m_prevTime > 0 && now > m_prevTime) {
|
|
double dt = (double)(now - m_prevTime) / 1e9;
|
|
int64_t df = frames - m_prevFrames;
|
|
if (dt > 0.05 && df >= 0)
|
|
m_fps = df / dt;
|
|
}
|
|
m_prevFrames = frames;
|
|
m_prevTime = now;
|
|
m_vals[1]->setText(conn && m_fps > 0.1
|
|
? QString::number(m_fps, 'f', 1)
|
|
: "-");
|
|
|
|
/* Uptime */
|
|
m_vals[2]->setText(
|
|
conn ? fmt_uptime(d->stats_connect_time_ns) : "-");
|
|
|
|
/* Data */
|
|
uint64_t tb = d->stats_total_bytes;
|
|
m_vals[3]->setText(tb > 0 ? fmt_bytes(tb) : "-");
|
|
|
|
/* Video */
|
|
if (d->stats_video_codec[0]) {
|
|
QString v = QString("Video: %1 %2\u00d7%3")
|
|
.arg(QString(d->stats_video_codec)
|
|
.toUpper())
|
|
.arg(d->stats_video_width)
|
|
.arg(d->stats_video_height);
|
|
if (d->stats_video_pixfmt[0])
|
|
v += QString(" %1").arg(d->stats_video_pixfmt);
|
|
v += QString(" \u00b7 Frames: %L1").arg(frames);
|
|
m_videoLine->setText(v);
|
|
} else {
|
|
m_videoLine->setText("-");
|
|
}
|
|
|
|
/* Audio */
|
|
if (d->stats_audio_codec[0])
|
|
m_audioLine->setText(
|
|
QString("Audio: %1 %2 Hz")
|
|
.arg(QString(d->stats_audio_codec)
|
|
.toUpper())
|
|
.arg(d->stats_audio_sample_rate));
|
|
else
|
|
m_audioLine->setText("-");
|
|
|
|
/* Server */
|
|
pthread_mutex_lock(&d->mutex);
|
|
int proto = d->protocol;
|
|
int port = d->port;
|
|
bool srtla = d->srtla_enabled;
|
|
int srtla_p = d->srtla_port;
|
|
pthread_mutex_unlock(&d->mutex);
|
|
|
|
QString s = QString("%1 \u00b7 Port %2")
|
|
.arg(proto == PROTOCOL_RTMP ? "RTMP"
|
|
: "SRT")
|
|
.arg(port);
|
|
if (srtla)
|
|
s += QString(" \u00b7 SRTLA \u2713 (:%1)")
|
|
.arg(srtla_p);
|
|
m_serverLine->setText(s);
|
|
}
|
|
|
|
void setNoSource()
|
|
{
|
|
m_dot->setStyleSheet(
|
|
"QLabel{background:#888;border-radius:5px;"
|
|
"min-width:10px;min-height:10px}");
|
|
m_status->setText(m_de ? "Keine Quelle" : "No source");
|
|
for (int c = 0; c < 4; c++)
|
|
m_vals[c]->setText("-");
|
|
m_videoLine->setText("-");
|
|
m_audioLine->setText("-");
|
|
m_serverLine->setText("-");
|
|
m_fps = 0;
|
|
m_prevFrames = 0;
|
|
m_prevTime = 0;
|
|
}
|
|
};
|
|
|
|
/* ---- dock creation ---- */
|
|
|
|
static QDockWidget *g_dock = nullptr;
|
|
|
|
extern "C" void stats_dialog_show(const char *locale)
|
|
{
|
|
if (g_dock) {
|
|
g_dock->setVisible(!g_dock->isVisible());
|
|
if (g_dock->isVisible()) {
|
|
g_dock->raise();
|
|
g_dock->activateWindow();
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool is_de = locale && (strncmp(locale, "de", 2) == 0);
|
|
|
|
QMainWindow *main = (QMainWindow *)obs_frontend_get_main_window();
|
|
if (!main)
|
|
return;
|
|
|
|
g_dock = new QDockWidget(
|
|
QString("Easy IRL Stream \u2014 Monitor"), main);
|
|
g_dock->setObjectName("EasyIRLStreamMonitorDock");
|
|
g_dock->setWidget(new StreamStatsWidget(is_de, g_dock));
|
|
g_dock->setAllowedAreas(Qt::AllDockWidgetAreas);
|
|
g_dock->setFeatures(QDockWidget::DockWidgetMovable |
|
|
QDockWidget::DockWidgetFloatable |
|
|
QDockWidget::DockWidgetClosable);
|
|
|
|
QObject::connect(g_dock, &QDockWidget::destroyed,
|
|
[]() { g_dock = nullptr; });
|
|
|
|
main->addDockWidget(Qt::BottomDockWidgetArea, g_dock);
|
|
g_dock->setFloating(true);
|
|
g_dock->resize(480, 230);
|
|
g_dock->show();
|
|
}
|