Initial commit
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
#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();
|
||||
}
|
||||
Reference in New Issue
Block a user