diff --git a/.github/scripts/utils.zsh/setup_linux b/.github/scripts/utils.zsh/setup_linux index 4bf0da75f9606e..f248f6ed861de4 100644 --- a/.github/scripts/utils.zsh/setup_linux +++ b/.github/scripts/utils.zsh/setup_linux @@ -109,6 +109,9 @@ if (( QT_VERSION == 6 )) { qt6-base-dev libqt6svg6-dev qt6-base-private-dev + libqt6opengl6-dev + libqt6openglwidgets6 + libqt6charts6-dev ) } else { log_error "Unsupported Qt version '${QT_VERSION}' specified." diff --git a/CMakeLists.txt b/CMakeLists.txt index b6f157fbe76011..5fcfa59f4a6e35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,10 @@ if(CMAKE_HOST_SYSTEM_NAME MATCHES "(Darwin)" OR OBS_CMAKE_VERSION VERSION_GREATE option(ENABLE_UI "Enable building with UI (requires Qt)" ON) option(ENABLE_SCRIPTING "Enable scripting support" ON) option(ENABLE_HEVC "Enable HEVC encoders" ON) + option(ENABLE_NEW_MPEGTS_OUTPUT "Use native SRT/RIST mpegts output" ON) + if(ENABLE_NEW_MPEGTS_OUTPUT) + add_compile_definitions(NEW_MPEGTS_OUTPUT) + endif() add_subdirectory(libobs) if(OS_WINDOWS) @@ -65,6 +69,10 @@ option(ENABLE_HEVC "Enable HEVC encoders" ON) if(ENABLE_HEVC) add_compile_definitions(ENABLE_HEVC) endif() +option(ENABLE_NEW_MPEGTS_OUTPUT "Use native SRT/RIST mpegts output" ON) +if(ENABLE_NEW_MPEGTS_OUTPUT) + add_compile_definitions(NEW_MPEGTS_OUTPUT) +endif() option(BUILD_FOR_DISTRIBUTION "Build for distribution (enables optimizations)" OFF) option(ENABLE_UI "Enable building with UI (requires Qt)" ON) option(ENABLE_SCRIPTING "Enable scripting support" ON) diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index 8221b9b8e506b7..4ffcd4d765f362 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -71,9 +71,9 @@ find_package(CURL REQUIRED) add_subdirectory(frontend-plugins) add_executable(obs) -find_qt(COMPONENTS Widgets Network Svg Xml COMPONENTS_LINUX Gui) +find_qt(COMPONENTS Widgets Network Svg Xml Charts COMPONENTS_LINUX Gui) -target_link_libraries(obs PRIVATE Qt::Widgets Qt::Svg Qt::Xml Qt::Network) +target_link_libraries(obs PRIVATE Qt::Widgets Qt::Svg Qt::Xml Qt::Network Qt::Charts) set_target_properties( obs diff --git a/UI/cmake/ui-qt.cmake b/UI/cmake/ui-qt.cmake index 6108dfd81e052f..967941fdb7b922 100644 --- a/UI/cmake/ui-qt.cmake +++ b/UI/cmake/ui-qt.cmake @@ -1,5 +1,5 @@ # cmake-format: off -find_package(Qt6 REQUIRED Widgets Network Svg Xml) +find_package(Qt6 REQUIRED Widgets Network Svg Xml Charts) # cmake-format: on if(OS_LINUX @@ -8,7 +8,7 @@ if(OS_LINUX find_package(Qt6 REQUIRED Gui) endif() -target_link_libraries(obs-studio PRIVATE Qt::Widgets Qt::Svg Qt::Xml Qt::Network) +target_link_libraries(obs-studio PRIVATE Qt::Widgets Qt::Svg Qt::Xml Qt::Network Qt::Charts) set_target_properties( obs-studio diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 9c3f387779133c..2fea1f818e069f 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -264,6 +264,20 @@ Basic.Stats.MegabytesSent="Total Data Output" Basic.Stats.Bitrate="Bitrate" Basic.Stats.DiskFullIn="Disk full in (approx.)" Basic.Stats.ResetStats="Reset Stats" +Basic.Stats.Dropped.Frames="Dropped Frames (%)" +Basic.Stats.Bitrate.kbps="Bitrate (kbps)" +Basic.Stats.Graphs="Live Graphs (Bitrate & Dropped Frames)" +Basic.Stats.Graph.Tooltip="zoom out: right-click.\n zoom in: select a vertical span." +Basic.Stats.Graphs.Show.Hide="Show / Hide Graphs." +Basic.Stats.Time.HMS="hh:mm:ss" +Basic.Stats.Time.Selector="Time span" +Basic.Stats.Time.Selector.MN="1 min" +Basic.Stats.Time.Selector.3MN="3 min" +Basic.Stats.Time.Selector.5MN="5 min" +Basic.Stats.Time.Selector.10MN="10 min" +Basic.Stats.Time.Selector.HR="1 hr" +Basic.Stats.Time.Selector.2HR="2 hr" +Basic.Stats.Time.Selector.4HR="4 hr" ResetUIWarning.Title="Are you sure you want to reset the UI?" ResetUIWarning.Text="Resetting the UI will hide additional docks. You will need to unhide these docks from the Docks menu if you want them to be visible.\n\nAre you sure you want to reset the UI?" diff --git a/UI/window-basic-stats.cpp b/UI/window-basic-stats.cpp index d403e717900a57..ffaeeec5b99d3e 100644 --- a/UI/window-basic-stats.cpp +++ b/UI/window-basic-stats.cpp @@ -23,6 +23,12 @@ void OBSBasicStats::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) OBSBasicStats *stats = reinterpret_cast(ptr); switch (event) { + case OBS_FRONTEND_EVENT_STREAMING_STARTED: + stats->chart->connect(); + break; + case OBS_FRONTEND_EVENT_STREAMING_STOPPED: + stats->chart->disconnect(); + break; case OBS_FRONTEND_EVENT_RECORDING_STARTED: stats->StartRecTimeLeft(); break; @@ -34,7 +40,6 @@ void OBSBasicStats::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) // window is being cleaned up. The closable stats window is // already gone by this point as it's deleted on close. obs_frontend_remove_event_callback(OBSFrontendEvent, stats); - break; default: break; } @@ -63,6 +68,7 @@ OBSBasicStats::OBSBasicStats(QWidget *parent, bool closable) { QVBoxLayout *mainLayout = new QVBoxLayout(); QGridLayout *topLayout = new QGridLayout(); + QVBoxLayout *chartLayout = new QVBoxLayout(); outputLayout = new QGridLayout(); bitrates.reserve(REC_TIME_LEFT_INTERVAL / TIMER_INTERVAL); @@ -118,8 +124,11 @@ OBSBasicStats::OBSBasicStats(QWidget *parent, bool closable) if (closable) closeButton = new QPushButton(QTStr("Close")); QPushButton *resetButton = new QPushButton(QTStr("Reset")); + QPushButton *showGraphs = + new QPushButton(QTStr("Basic.Stats.Graphs.Show.Hide")); QHBoxLayout *buttonLayout = new QHBoxLayout; buttonLayout->addStretch(); + buttonLayout->addWidget(showGraphs); buttonLayout->addWidget(resetButton); if (closable) buttonLayout->addWidget(closeButton); @@ -157,10 +166,52 @@ OBSBasicStats::OBSBasicStats(QWidget *parent, bool closable) scrollArea->setWidget(widget); scrollArea->setWidgetResizable(true); + /* --------------------------------------------- */ + QLabel *chartGroupLabel = new QLabel(QTStr("Basic.Stats.Graphs")); + chartGroupLabel->setStyleSheet("font-weight: bold"); + QLabel *timeLabel = new QLabel(QTStr("Basic.Stats.Time.Selector")); + timeLabel->setStyleSheet("font-weight: bold"); + timeBox = new QComboBox(); + timeBox->addItem(QTStr("Basic.Stats.Time.Selector.MN"), + (int)OBSCHART_TIME_MN); + timeBox->addItem(QTStr("Basic.Stats.Time.Selector.3MN"), + (int)OBSCHART_TIME_3MN); + timeBox->addItem(QTStr("Basic.Stats.Time.Selector.5MN"), + (int)OBSCHART_TIME_5MN); + timeBox->addItem(QTStr("Basic.Stats.Time.Selector.10MN"), + (int)OBSCHART_TIME_10MN); + timeBox->addItem(QTStr("Basic.Stats.Time.Selector.HR"), + (int)OBSCHART_TIME_HR); + timeBox->addItem(QTStr("Basic.Stats.Time.Selector.2HR"), + (int)OBSCHART_TIME_2HR); + timeBox->addItem(QTStr("Basic.Stats.Time.Selector.4HR"), + (int)OBSCHART_TIME_4HR); + connect(timeBox, &QComboBox::currentIndexChanged, this, + &OBSBasicStats::timeBoxItemChanged); + timeGroupBox = new QGroupBox(); + QGridLayout *timeLayout = new QGridLayout(timeGroupBox); + + chart = new OBSStatsChart(); + QChartView *chartView = new QChartView(chart); + chartView->setRenderHint(QPainter::Antialiasing); + chartView->setMinimumSize(600, 300); + chartView->setRubberBand(QChartView::VerticalRubberBand); + chartView->setToolTip(QTStr("Basic.Stats.Graph.Tooltip")); + chartView->setToolTipDuration(2000); + timeLayout->addWidget(chartGroupLabel, 0, 0, 1, 2, Qt::AlignVCenter); + timeLayout->addWidget(chartView, 1, 0, 1, 5, + Qt::AlignVCenter | Qt::AlignLeft); + timeLayout->addWidget(timeLabel, 2, 1, 1, 1, + Qt::AlignVCenter | Qt::AlignRight); + timeLayout->addWidget(timeBox, 2, 2, 1, 1, + Qt::AlignVCenter | Qt::AlignLeft); + chartLayout->addWidget(timeGroupBox); + /* --------------------------------------------- */ mainLayout->addLayout(topLayout); mainLayout->addWidget(scrollArea); + mainLayout->addLayout(chartLayout); mainLayout->addLayout(buttonLayout); setLayout(mainLayout); @@ -169,7 +220,8 @@ OBSBasicStats::OBSBasicStats(QWidget *parent, bool closable) connect(closeButton, &QPushButton::clicked, [this]() { close(); }); connect(resetButton, &QPushButton::clicked, [this]() { Reset(); }); - + connect(showGraphs, &QPushButton::clicked, + [this]() { ToggleGraphs(); }); delete shortcutFilter; shortcutFilter = CreateShortcutFilter(); installEventFilter(shortcutFilter); @@ -439,6 +491,7 @@ void OBSBasicStats::Update() long double kbps = outputLabels[1].kbps; bitrates.push_back(kbps); } + chart->output = strOutput; } void OBSBasicStats::StartRecTimeLeft() @@ -499,6 +552,16 @@ void OBSBasicStats::Reset() outputLabels[0].Reset(strOutput); outputLabels[1].Reset(recOutput); Update(); + if (obs_frontend_streaming_active()) { + chart->disconnect(); + chart->connect(); + } +} + +void OBSBasicStats::ToggleGraphs() +{ + bool is_visible = timeGroupBox->isVisible(); + timeGroupBox->setVisible(!is_visible); } void OBSBasicStats::OutputLabels::Update(obs_output_t *output, bool rec) @@ -613,3 +676,204 @@ void OBSBasicStats::hideEvent(QHideEvent *) { timer.stop(); } + +void OBSBasicStats::timeBoxItemChanged(int index) +{ + int time_mn = timeBox->itemData(index).toInt(); + chart->changeTimeSpan(time_mn); +} + +OBSStatsChart::OBSStatsChart(QGraphicsItem *parent, Qt::WindowFlags wFlags) + : QChart(QChart::ChartTypeCartesian, parent, wFlags), + bitrate_series(new QSplineSeries), + dropped_series(new QSplineSeries), + dummy_series(new QLineSeries), + m_axisX(new QValueAxis()), + m_axisY(new QValueAxis()), + m_axisZ(new QValueAxis()), + m_axisT(new QDateTimeAxis()), + m_x(0), + m_y(0), + m_t(0), + time_span(1), + output(nullptr) +{ + QPen red(Qt::red); + QPen green(Qt::green); + QPen blue(Qt::blue); + red.setWidth(3); + green.setWidth(3); + blue.setWidth(3); + bitrate_series->setPen(red); + dropped_series->setPen(blue); + + bitrate_series->setName(QTStr("Basic.Stats.Bitrate")); + dropped_series->setName(QTStr("Basic.Stats.DroppedFrames")); + + addSeries(dropped_series); + addSeries(bitrate_series); + addSeries(dummy_series); + + addAxis(m_axisX, Qt::AlignTop); + addAxis(m_axisY, Qt::AlignLeft); + addAxis(m_axisZ, Qt::AlignRight); + addAxis(m_axisT, Qt::AlignBottom); + m_axisX->setVisible(false); + + bitrate_series->attachAxis(m_axisX); + bitrate_series->attachAxis(m_axisY); + dummy_series->attachAxis(m_axisT); + dummy_series->attachAxis(m_axisY); + dummy_series->hide(); + dropped_series->attachAxis(m_axisX); + dropped_series->attachAxis(m_axisZ); + + m_axisX->setTickCount(5); + m_axisX->setRange(0, 1); + m_axisY->setRange(0, 10000); + m_axisY->setTitleText(QTStr("Basic.Stats.Bitrate.kbps")); + m_axisZ->setRange(0, 10); + m_axisZ->setTitleText(QTStr("Basic.Stats.Dropped.Frames")); + + m_axisT->setTickCount(5); + m_axisT->setFormat("hh:mm:ss"); + m_axisT->setTitleText(QTStr("Basic.Stats.Time.HMS")); + + QTime tm = QTime::currentTime(); + QTime tM = tm.addSecs(60); + QDate date = QDate::currentDate(); + QDateTime min; + min.setDate(date); + min.setTime(tm); + QDateTime max; + max.setDate(date); + max.setTime(tM); + + m_axisT->setRange(min, max); + + m_timer.start(); +} + +void OBSStatsChart::connect() +{ + // Reset the chart when there's a stream starting. + m_axisX->setTickCount(5); + m_axisX->setRange(0, 60 * time_span); + m_axisY->setRange(0, 10000); + m_axisZ->setRange(0, 10); + m_x = 0; + m_y = 0; + m_t = 0; + bitrate_series->clear(); + dropped_series->clear(); + first_total = obs_output_get_total_frames(output); + first_dropped = obs_output_get_frames_dropped(output); + + QTime tm = QTime::currentTime(); + QTime tM = tm.addSecs(60); + QDate date = QDate::currentDate(); + QDateTime min; + min.setDate(date); + min.setTime(tm); + QDateTime max; + max.setDate(date); + max.setTime(tM); + m_axisT->setRange(min, max); + + // Refresh the chart every second. + QObject::connect(&m_timer, &QTimer::timeout, this, + &OBSStatsChart::handleTimeout); + m_timer.setInterval(1000); +} + +void OBSStatsChart::disconnect() +{ + QObject::disconnect(&m_timer, &QTimer::timeout, this, + &OBSStatsChart::handleTimeout); +} + +void OBSStatsChart::changeTimeSpan(int time_mn) +{ + time_span = time_mn; + m_axisX->setRange(m_axisX->max() - time_span * 60, m_axisX->max()); + QDateTime max = m_axisT->max(); + QDate date = QDate::currentDate(); + QDateTime min; + min.setDate(date); + QTime tm = max.time().addSecs(-60 * time_span); + min.setTime(tm); + m_axisT->setRange(min, max); +} + +OBSStatsChart::~OBSStatsChart() {} + +void OBSStatsChart::handleTimeout() +{ + // The real width is divided in 60 slots per minute. + qreal slide_x = plotArea().width() / (60 * time_span); + qreal y = 1; + // Retrieve data from output. + uint64_t totalBytes = output ? obs_output_get_total_bytes(output) : 0; + 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; + kbps = (long double)bitsBetween / timePassed / 1000.0l; + + if (timePassed < 0.01l) + kbps = 0.0l; + + // Add bitrate data to chart & auto-scale the axis. + m_x += y; + m_y = kbps; + + lastBytesSent = bytesSent; + lastBytesSentTime = curTime; + + // auto-scale the Y axis (bitrate) + if (m_x == 1) + m_axisY->setRange(0, qMax(qCeil(m_y / 10) * 10, 10000)); + if (m_axisY->min() < 0) + m_axisY->setRange(0, 10.0 * qCeil(m_axisY->max() / 10)); + if (m_y >= 0.8 * m_axisY->max()) + m_axisY->setRange(0, qMax(2 * m_axisY->max(), + 20.0 * qCeil(m_y / 10))); + + bitrate_series->append(m_x, m_y); + + // Add dropped frames to chart & auto-scale the axis. + long double num = (long double)totalBytes / (1024.0l * 1024.0l); + int total = output ? obs_output_get_total_frames(output) : 0; + int dropped = output ? obs_output_get_frames_dropped(output) : 0; + + 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; + + m_y = num; + if (m_x == 1) + m_axisY->setRange(0, qMax(qCeil(m_y / 10) * 10, 10)); + if (m_axisZ->min() < 0) + m_axisZ->setRange(0, 10.0 * qCeil(m_axisZ->max() / 10)); + if (m_y >= 0.8 * m_axisZ->max()) + m_axisZ->setRange(0, qMax(2 * m_axisZ->max(), + 20.0 * qCeil(m_y / 10))); + dropped_series->append(m_x, m_y); + + // Scroll once the whole range has been drawn + if (m_x > 60) + scroll(slide_x, 0); +} diff --git a/UI/window-basic-stats.hpp b/UI/window-basic-stats.hpp index c1d434bf65baf2..1b83d4d1fe169a 100644 --- a/UI/window-basic-stats.hpp +++ b/UI/window-basic-stats.hpp @@ -9,9 +9,66 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + class QGridLayout; class QCloseEvent; +enum OBSCHART_TIME_SPAN { + OBSCHART_TIME_MN = 1, + OBSCHART_TIME_3MN = 3, + OBSCHART_TIME_5MN = 5, + OBSCHART_TIME_10MN = 10, + OBSCHART_TIME_HR = 60, + OBSCHART_TIME_2HR = 120, + OBSCHART_TIME_4HR = 240 +}; + +class OBSStatsChart : public QChart { + Q_OBJECT + +public: + OBSStatsChart(QGraphicsItem *parent = nullptr, + Qt::WindowFlags wFlags = {}); + virtual ~OBSStatsChart(); + QValueAxis *m_axisX; // dummy time axis used for scrolling + QValueAxis *m_axisY; // bitrate + QValueAxis *m_axisZ; // dropped frames + QDateTimeAxis *m_axisT; // time axis in QDateTime format + QSplineSeries *bitrate_series = nullptr; + QSplineSeries *dropped_series = nullptr; + QLineSeries *dummy_series = nullptr; + obs_output_t *output; + uint time_span; // horizontal axis width in minutes + uint64_t lastBytesSent = 0; + uint64_t lastBytesSentTime = 0; + int first_total = 0; + int first_dropped = 0; + long double kbps = 0.0l; + +public slots: + void handleTimeout(); + void connect(); + void disconnect(); + void changeTimeSpan(int time); + +private: + QTimer m_timer; + qreal m_x; + qreal m_y; + qreal m_t; +}; + class OBSBasicStats : public QFrame { Q_OBJECT @@ -26,6 +83,7 @@ class OBSBasicStats : public QFrame { QLabel *missedFrames = nullptr; QGridLayout *outputLayout = nullptr; + QGroupBox *timeGroupBox = nullptr; os_cpu_usage_info_t *cpu_info = nullptr; @@ -34,6 +92,10 @@ class OBSBasicStats : public QFrame { uint64_t num_bytes = 0; std::vector bitrates; + OBSStatsChart *chart = nullptr; + QChartView *chartView = nullptr; + QComboBox *timeBox = nullptr; + struct OutputLabels { QPointer name; QPointer status; @@ -76,9 +138,11 @@ class OBSBasicStats : public QFrame { private slots: void RecordingTimeLeft(); + void timeBoxItemChanged(int index); public slots: void Reset(); + void ToggleGraphs(); protected: virtual void showEvent(QShowEvent *event) override; diff --git a/buildspec.json b/buildspec.json index 080a78db3ce4bf..75f34975d1ce03 100644 --- a/buildspec.json +++ b/buildspec.json @@ -11,15 +11,15 @@ } }, "qt6": { - "version": "2024-01-27", - "baseUrl": "https://github.com/obsproject/obs-deps/releases/download", + "version": "2024-02-19", + "baseUrl": "https://github.com/pkviet/obs-deps/releases/download", "label": "Pre-Built Qt6", "hashes": { - "macos-universal": "86f6418c3e454b3979947fdba183aa7303ca9d7729c7056435bd023d466cf93a", - "windows-x64": "7a15c4b494bfe2df51cad16e7cbcfcc2b5b69b55839dd584c622214038c7d44c" + "macos-universal": "77bb4ea468524f1f1e4c3c83fb72251b129716390d1918a49b726cdc306ac1fb", + "windows-x64": "9e5c59f3f26690931808ee1173cae3bc912b6058edbeebc71762ac598f8f901a" }, "debugSymbols": { - "windows-x64": "ddf356ce20a4946f7522253b5da44ce7fe29398bb55ff74467431fb31e8ec645" + "windows-x64": "62f993967e3858e7e75ef1ef2d42aef2d9d6474b81bbbc062a92a850abd8159b" } }, "cef": { diff --git a/cmake/Modules/CopyMSVCBins.cmake b/cmake/Modules/CopyMSVCBins.cmake index 390a49edf51643..542e0915c71950 100644 --- a/cmake/Modules/CopyMSVCBins.cmake +++ b/cmake/Modules/CopyMSVCBins.cmake @@ -182,6 +182,9 @@ file( "${QtCore_BIN_DIR}/Qt6Svgd.dll" "${QtCore_BIN_DIR}/Qt6Xmld.dll" "${QtCore_BIN_DIR}/Qt6Networkd.dll" + "${QtCore_BIN_DIR}/Qt6Chartsd.dll" + "${QtCore_BIN_DIR}/Qt6OpenGLd.dll" + "${QtCore_BIN_DIR}/Qt6OpenGLWidgetsd.dll" "${QtCore_BIN_DIR}/libGLESv2d.dll" "${QtCore_BIN_DIR}/libEGLd.dll") file(GLOB QT_DEBUG_PLAT_BIN_FILES "${QtCore_PLUGIN_DIR}/platforms/qwindowsd.dll") @@ -199,6 +202,9 @@ file( "${QtCore_BIN_DIR}/Qt6Svg.dll" "${QtCore_BIN_DIR}/Qt6Xml.dll" "${QtCore_BIN_DIR}/Qt6Network.dll" + "${QtCore_BIN_DIR}/Qt6Charts.dll" + "${QtCore_BIN_DIR}/Qt6OpenGL.dll" + "${QtCore_BIN_DIR}/Qt6OpenGLWidgets.dll" "${QtCore_BIN_DIR}/libGLESv2.dll" "${QtCore_BIN_DIR}/libEGL.dll") file(GLOB QT_PLAT_BIN_FILES "${QtCore_PLUGIN_DIR}/platforms/qwindows.dll") diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 90bea35be27345..8ad6b897a0254d 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -62,6 +62,9 @@ if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) check_obs_browser() add_subdirectory(obs-ffmpeg) add_subdirectory(obs-filters) + if(ENABLE_NEW_MPEGTS_OUTPUT) + add_subdirectory(obs-mpegts) + endif() add_subdirectory(obs-outputs) if(OS_WINDOWS) add_subdirectory(obs-qsv11) @@ -185,3 +188,6 @@ add_subdirectory(rtmp-services) add_subdirectory(text-freetype2) add_subdirectory(aja) add_subdirectory(obs-webrtc) +if(ENABLE_NEW_MPEGTS_OUTPUT) + add_subdirectory(obs-mpegts) +endif() diff --git a/plugins/obs-ffmpeg/CMakeLists.txt b/plugins/obs-ffmpeg/CMakeLists.txt index 77c5ad8e4950bb..4d017f6d35efc1 100644 --- a/plugins/obs-ffmpeg/CMakeLists.txt +++ b/plugins/obs-ffmpeg/CMakeLists.txt @@ -3,7 +3,6 @@ cmake_minimum_required(VERSION 3.22...3.25) legacy_check() option(ENABLE_FFMPEG_LOGGING "Enables obs-ffmpeg logging" OFF) -option(ENABLE_NEW_MPEGTS_OUTPUT "Use native SRT/RIST mpegts output" ON) include(cmake/dependencies.cmake) @@ -16,10 +15,6 @@ target_sources( obs-ffmpeg PRIVATE # cmake-format: sortable $<$:obs-ffmpeg-logging.c> - $<$:obs-ffmpeg-mpegts.c> - $<$:obs-ffmpeg-rist.h> - $<$:obs-ffmpeg-srt.h> - $<$:obs-ffmpeg-url.h> $<$:obs-ffmpeg-vaapi.c> $<$:vaapi-utils.c> $<$:vaapi-utils.h> @@ -65,9 +60,7 @@ target_link_libraries( $<$:ws2_32> $<$:Libva::va> $<$:Libva::drm> - $<$:Libpci::pci> - $<$:Librist::Librist> - $<$:Libsrt::Libsrt>) + $<$:Libpci::pci>) if(OS_WINDOWS) configure_file(cmake/windows/obs-module.rc.in obs-ffmpeg.rc) diff --git a/plugins/obs-ffmpeg/cmake/dependencies.cmake b/plugins/obs-ffmpeg/cmake/dependencies.cmake index 5a52c751ac24ac..f79f11dfd4369d 100644 --- a/plugins/obs-ffmpeg/cmake/dependencies.cmake +++ b/plugins/obs-ffmpeg/cmake/dependencies.cmake @@ -42,22 +42,3 @@ elseif( find_package(Libva REQUIRED) find_package(Libpci REQUIRED) endif() - -if(ENABLE_NEW_MPEGTS_OUTPUT) - find_package(Librist QUIET) - find_package(Libsrt QUIET) - - foreach(_output_lib IN ITEMS Librist Libsrt) - if(NOT TARGET ${_output_lib}::${_output_lib}) - list(APPEND _error_messages "MPEGTS output library ${_output_lib} not found.") - endif() - endforeach() - - if(_error_messages) - list(JOIN _error_messages "\n" _error_string) - message( - FATAL_ERROR - "${_error_string}\n Disable this error by setting ENABLE_NEW_MPEGTS_OUTPUT to OFF or providing the build system with required SRT and Rist libraries." - ) - endif() -endif() diff --git a/plugins/obs-ffmpeg/cmake/legacy.cmake b/plugins/obs-ffmpeg/cmake/legacy.cmake index cf92a9ea58d7e8..32755cf0e610ca 100644 --- a/plugins/obs-ffmpeg/cmake/legacy.cmake +++ b/plugins/obs-ffmpeg/cmake/legacy.cmake @@ -1,7 +1,6 @@ project(obs-ffmpeg) option(ENABLE_FFMPEG_LOGGING "Enables obs-ffmpeg logging" OFF) -option(ENABLE_NEW_MPEGTS_OUTPUT "Use native SRT/RIST mpegts output" ON) find_package( FFmpeg REQUIRED @@ -17,20 +16,6 @@ add_library(obs-ffmpeg MODULE) add_library(OBS::ffmpeg ALIAS obs-ffmpeg) add_subdirectory(ffmpeg-mux) -if(ENABLE_NEW_MPEGTS_OUTPUT) - find_package(Librist QUIET) - find_package(Libsrt QUIET) - - if(NOT TARGET Librist::Librist AND NOT TARGET Libsrt::Libsrt) - obs_status( - FATAL_ERROR - "SRT and RIST libraries not found! Please install SRT and RIST libraries or set ENABLE_NEW_MPEGTS_OUTPUT=OFF.") - elseif(NOT TARGET Libsrt::Libsrt) - obs_status(FATAL_ERROR "SRT library not found! Please install SRT library or set ENABLE_NEW_MPEGTS_OUTPUT=OFF.") - elseif(NOT TARGET Librist::Librist) - obs_status(FATAL_ERROR "RIST library not found! Please install RIST library or set ENABLE_NEW_MPEGTS_OUTPUT=OFF.") - endif() -endif() configure_file(${CMAKE_CURRENT_SOURCE_DIR}/obs-ffmpeg-config.h.in ${CMAKE_BINARY_DIR}/config/obs-ffmpeg-config.h) @@ -66,16 +51,6 @@ target_link_libraries( FFmpeg::swscale FFmpeg::swresample) -if(ENABLE_NEW_MPEGTS_OUTPUT) - target_sources(obs-ffmpeg PRIVATE obs-ffmpeg-mpegts.c obs-ffmpeg-srt.h obs-ffmpeg-rist.h obs-ffmpeg-url.h) - - target_link_libraries(obs-ffmpeg PRIVATE Librist::Librist Libsrt::Libsrt) - if(OS_WINDOWS) - target_link_libraries(obs-ffmpeg PRIVATE ws2_32.lib) - endif() - target_compile_definitions(obs-ffmpeg PRIVATE NEW_MPEGTS_OUTPUT) -endif() - if(ENABLE_FFMPEG_LOGGING) target_sources(obs-ffmpeg PRIVATE obs-ffmpeg-logging.c) endif() diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-output.h b/plugins/obs-ffmpeg/obs-ffmpeg-output.h index 7b898f9a21d9d8..6d1c030a7a011a 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-output.h +++ b/plugins/obs-ffmpeg/obs-ffmpeg-output.h @@ -5,9 +5,6 @@ #include #include #include -#ifdef NEW_MPEGTS_OUTPUT -#include "obs-ffmpeg-url.h" -#endif struct ffmpeg_cfg { const char *url; @@ -107,12 +104,6 @@ struct ffmpeg_output { os_event_t *stop_event; DARRAY(AVPacket *) packets; -#ifdef NEW_MPEGTS_OUTPUT - /* used for SRT & RIST */ - URLContext *h; - AVIOContext *s; - bool got_headers; -#endif }; bool ffmpeg_data_init(struct ffmpeg_data *data, struct ffmpeg_cfg *config); void ffmpeg_data_free(struct ffmpeg_data *data); diff --git a/plugins/obs-ffmpeg/obs-ffmpeg.c b/plugins/obs-ffmpeg/obs-ffmpeg.c index bca4ca1153988c..c355bf64775ace 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg.c @@ -27,7 +27,9 @@ MODULE_EXPORT const char *obs_module_description(void) extern struct obs_source_info ffmpeg_source; extern struct obs_output_info ffmpeg_output; extern struct obs_output_info ffmpeg_muxer; +#ifndef NEW_MPEGTS_OUTPUT extern struct obs_output_info ffmpeg_mpegts_muxer; +#endif extern struct obs_output_info replay_buffer; extern struct obs_output_info ffmpeg_hls_muxer; extern struct obs_encoder_info aac_encoder_info; @@ -362,7 +364,9 @@ bool obs_module_load(void) obs_register_source(&ffmpeg_source); obs_register_output(&ffmpeg_output); obs_register_output(&ffmpeg_muxer); +#ifndef NEW_MPEGTS_OUTPUT obs_register_output(&ffmpeg_mpegts_muxer); +#endif obs_register_output(&ffmpeg_hls_muxer); obs_register_output(&replay_buffer); obs_register_encoder(&aac_encoder_info); diff --git a/plugins/obs-mpegts/CMakeLists.txt b/plugins/obs-mpegts/CMakeLists.txt new file mode 100644 index 00000000000000..4b25f9e74b3727 --- /dev/null +++ b/plugins/obs-mpegts/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.22...3.25) + +legacy_check() + +include(cmake/dependencies.cmake) + +add_library(obs-mpegts MODULE) +add_library(OBS::mpegts ALIAS obs-mpegts) + +find_package(Librist REQUIRED) +find_package(Libsrt REQUIRED) + +target_sources(obs-mpegts PRIVATE obs-mpegts.c obs-mpegts-common.h obs-mpegts-rist.h obs-mpegts-srt.h + obs-mpegts-srt-stats.cpp obs-mpegts-srt-stats.hpp) + +target_compile_options(obs-ffmpeg PRIVATE $<$:-Wno-shorten-64-to-32>) +target_link_libraries( + obs-mpegts + PRIVATE OBS::libobs + OBS::frontend-api + FFmpeg::avcodec + FFmpeg::avformat + FFmpeg::avutil + $<$:OBS::w32-pthreads> + $<$:ws2_32> + Librist::Librist + Libsrt::Libsrt + Qt::Core + Qt::Widgets + Qt::Charts) + +if(OS_WINDOWS) + configure_file(cmake/windows/obs-module.rc.in obs-mpegts.rc) + target_sources(obs-mpegts PRIVATE obs-mpegts.rc) +endif() + +# cmake-format: off +set_target_properties_obs(obs-mpegts PROPERTIES FOLDER plugins/obs-mpegts PREFIX "" AUTOMOC ON AUTOUIC ON AUTORCC ON) +# cmake-format: on diff --git a/plugins/obs-mpegts/cmake/dependencies.cmake b/plugins/obs-mpegts/cmake/dependencies.cmake new file mode 100644 index 00000000000000..342283905aecf0 --- /dev/null +++ b/plugins/obs-mpegts/cmake/dependencies.cmake @@ -0,0 +1,31 @@ +# cmake-format: off +if(OS_WINDOWS OR OS_MACOS) + set(ffmpeg_version 6) +else() + set(ffmpeg_version 4.4) +endif() + +find_package( + FFmpeg ${ffmpeg_version} + REQUIRED avcodec + avutil + avformat) +# cmake-format: on + +find_qt(COMPONENTS Core Widgets Charts) +find_package(Librist QUIET) +find_package(Libsrt QUIET) + +foreach(_output_lib IN ITEMS Librist Libsrt) + if(NOT TARGET ${_output_lib}::${_output_lib}) + list(APPEND _error_messages "MPEGTS output library ${_output_lib} not found.") + endif() +endforeach() + +if(_error_messages) + list(JOIN "\n" _error_string _error_string) + message( + FATAL_ERROR + "${_error_string}\n Disable this error by setting ENABLE_NEW_MPEGTS_OUTPUT to OFF or providing the build system with required SRT and Rist libraries." + ) +endif() diff --git a/plugins/obs-mpegts/cmake/legacy.cmake b/plugins/obs-mpegts/cmake/legacy.cmake new file mode 100644 index 00000000000000..bf583e0bb2efba --- /dev/null +++ b/plugins/obs-mpegts/cmake/legacy.cmake @@ -0,0 +1,59 @@ +project(obs-mpegts) + +find_package(FFmpeg REQUIRED COMPONENTS avcodec avutil avformat) + +add_library(obs-mpegts MODULE) +add_library(OBS::mpegts ALIAS obs-mpegts) + +find_package(Librist REQUIRED) +find_package(Libsrt REQUIRED) +find_qt(COMPONENTS Core Widgets Charts) + +foreach(_output_lib IN ITEMS Librist Libsrt) + if(NOT TARGET ${_output_lib}::${_output_lib}) + list(APPEND _error_messages "MPEGTS output library ${_output_lib} not found.") + endif() +endforeach() + +if(_error_messages) + list(JOIN "\n" _error_string _error_string) + message( + FATAL_ERROR + "${_error_string}\n Disable this error by setting ENABLE_NEW_MPEGTS_OUTPUT to OFF or providing the build system with required SRT and Rist libraries." + ) +endif() + +target_sources(obs-mpegts PRIVATE obs-mpegts.c obs-mpegts-common.h obs-mpegts-rist.h obs-mpegts-srt.h + obs-mpegts-srt-stats.cpp obs-mpegts-srt-stats.hpp) + +target_link_libraries( + obs-mpegts + PRIVATE OBS::libobs + OBS::frontend-api + FFmpeg::avcodec + FFmpeg::avformat + FFmpeg::avutil + Librist::Librist + Libsrt::Libsrt + Qt::Core + Qt::Widgets + Qt::Charts) + +set_target_properties(obs-mpegts PROPERTIES FOLDER "plugins/obs-mpegts" PREFIX "") +set_target_properties( + obs-mpegts + PROPERTIES AUTOMOC ON + AUTOUIC ON + AUTORCC ON) + +if(OS_WINDOWS) + target_link_libraries(obs-mpegts PRIVATE ws2_32.lib) + if(MSVC) + target_link_libraries(obs-mpegts PRIVATE OBS::w32-pthreads) + endif() + + set(MODULE_DESCRIPTION "OBS MPEG-TS muxer module") + configure_file(${CMAKE_SOURCE_DIR}/cmake/bundle/windows/obs-module.rc.in obs-mpegts.rc) +endif() + +setup_plugin_target(obs-mpegts) diff --git a/plugins/obs-mpegts/cmake/windows/obs-module.rc.in b/plugins/obs-mpegts/cmake/windows/obs-module.rc.in new file mode 100644 index 00000000000000..c0e0656eef471e --- /dev/null +++ b/plugins/obs-mpegts/cmake/windows/obs-module.rc.in @@ -0,0 +1,24 @@ +1 VERSIONINFO +FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "${OBS_COMPANY_NAME}" + VALUE "FileDescription", "OBS MPEG-TS muxer module" + VALUE "FileVersion", "${OBS_VERSION_CANONICAL}" + VALUE "ProductName", "${OBS_PRODUCT_NAME}" + VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}" + VALUE "Comments", "${OBS_COMMENTS}" + VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}" + VALUE "InternalName", "obs-mpegts" + VALUE "OriginalFilename", "obs-mpegts" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/plugins/obs-mpegts/data/locale/en-US.ini b/plugins/obs-mpegts/data/locale/en-US.ini new file mode 100644 index 00000000000000..0dd5b4ed9f6001 --- /dev/null +++ b/plugins/obs-mpegts/data/locale/en-US.ini @@ -0,0 +1,35 @@ +FFmpegMpegts="FFmpeg MPEG-TS Muxer" + +SRT.Stats="SRT Stats" +SRT.Stats.Reset="Reset" +SRT.Stats.Status="Status" +SRT.Stats.Status.Live="LIVE" +SRT.Stats.Status.Reconnecting="Reconnecting" +SRT.Stats.Status.Inactive="Inactive" +SRT.Stats.Status.Active="Active" +SRT.Stats.DroppedFrames="Dropped Frames" +SRT.Stats.MegabytesSent="Total Data Output" +SRT.Stats.Bitrate="Bitrate" +SRT.Stats.RTT="RTT" +SRT.Stats.RTT.ms="RTT (ms)" +SRT.Stats.Bandwidth="Link Bandwidth" +SRT.Stats.Packets="Packets per second" +SRT.Stats.Total.Pkts="Total Sent" +SRT.Stats.Retransmitted.Pkts="Total Retransmitted" +SRT.Stats.Retransmitted.Pkts.Rate="Retransmitted (pkt/sec)." +SRT.Stats.Dropped.Pkts="Total Dropped" +SRT.Stats.Dropped.Pkts.Rate="Dropped (pkt/sec)." +SRT.Stats.Peer.Latency="Peer Latency" +SRT.Stats.Latency="Latency" +SRT.Stats.Time="Time elapsed (minutes)" +SRT.Stats.Time.HMS="Time (hh:mm:ss)" +SRT.Stats.Time.Selector="Chart time span" +SRT.Stats.Time.Selector.MN="1 min" +SRT.Stats.Time.Selector.3MN="3 min" +SRT.Stats.Time.Selector.5MN="5 min" +SRT.Stats.Time.Selector.10MN="10 min" +SRT.Stats.Time.Selector.HR="1 hr" +SRT.Stats.Time.Selector.2HR="2 hr" +SRT.Stats.Time.Selector.4HR="4 hr" +SRT.Stats.Warning="Warning" +SRT.Stats.Tooltip="Right-click to zoom out.\nSelect a vertical span to zoom in." diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-url.h b/plugins/obs-mpegts/obs-mpegts-common.h similarity index 55% rename from plugins/obs-ffmpeg/obs-ffmpeg-url.h rename to plugins/obs-mpegts/obs-mpegts-common.h index 2dd18599cd986f..17c39023009c13 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-url.h +++ b/plugins/obs-mpegts/obs-mpegts-common.h @@ -1,10 +1,24 @@ #pragma once +#include +#include +#include +#include +#include +#include + +#include +#include #include #include #include #include #include #include +#include +#include +#include +#include + #include #include @@ -23,6 +37,109 @@ typedef struct URLContext { int64_t rw_timeout; /* max time to wait for write completion in mcs */ } URLContext; +struct mpegts_cfg { + const char *url; + const char *format_name; + const char *format_mime_type; + const char *muxer_settings; + const char *protocol_settings; // not used yet for SRT nor RIST + int gop_size; + int video_bitrate; + int audio_bitrate; + const char *video_encoder; + int video_encoder_id; + const char *audio_encoder; + int audio_encoder_id; + int audio_bitrates[MAX_AUDIO_MIXES]; // multi-track + const char *video_settings; + const char *audio_settings; + int audio_mix_count; + int audio_tracks; + enum AVPixelFormat format; + enum AVColorRange color_range; + enum AVColorPrimaries color_primaries; + enum AVColorTransferCharacteristic color_trc; + enum AVColorSpace colorspace; + int max_luminance; + int scale_width; + int scale_height; + int width; + int height; + int frame_size; // audio frame size + const char *username; + const char *password; + const char *stream_id; + const char *encrypt_passphrase; +}; + +struct mpegts_audio_info { + AVStream *stream; + AVCodecContext *ctx; +}; + +struct mpegts_data { + AVStream *video; + AVCodecContext *video_ctx; + struct mpegts_audio_info *audio_infos; + const AVCodec *acodec; + const AVCodec *vcodec; + AVFormatContext *output; + struct SwsContext *swscale; + + int64_t total_frames; + AVFrame *vframe; + int frame_size; + + uint64_t start_timestamp; + + int64_t total_samples[MAX_AUDIO_MIXES]; + uint32_t audio_samplerate; + enum audio_format audio_format; + size_t audio_planes; + size_t audio_size; + int num_audio_streams; + + /* audio_tracks is a bitmask storing the indices of the mixes */ + int audio_tracks; + struct deque excess_frames[MAX_AUDIO_MIXES][MAX_AV_PLANES]; + uint8_t *samples[MAX_AUDIO_MIXES][MAX_AV_PLANES]; + AVFrame *aframe[MAX_AUDIO_MIXES]; + + struct mpegts_cfg config; + + bool initialized; + + char *last_error; +}; + +struct mpegts_output { + obs_output_t *output; + volatile bool active; + struct mpegts_data ff_data; + + bool connecting; + pthread_t start_thread; + + uint64_t total_bytes; + + uint64_t audio_start_ts; + uint64_t video_start_ts; + uint64_t stop_ts; + volatile bool stopping; + + bool write_thread_active; + pthread_mutex_t write_mutex; + pthread_t write_thread; + os_sem_t *write_sem; + os_event_t *stop_event; + + DARRAY(AVPacket *) packets; + /* used for SRT & RIST */ + URLContext *h; + AVIOContext *s; + bool got_headers; +}; + #define UDP_DEFAULT_PAYLOAD_SIZE 1316 /* We need to override libsrt/win/syslog_defs.h due to conflicts w/ some libobs diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-rist.h b/plugins/obs-mpegts/obs-mpegts-rist.h similarity index 99% rename from plugins/obs-ffmpeg/obs-ffmpeg-rist.h rename to plugins/obs-mpegts/obs-mpegts-rist.h index 215f80ae181c86..d6b4b186aaa28c 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-rist.h +++ b/plugins/obs-mpegts/obs-mpegts-rist.h @@ -14,10 +14,8 @@ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ - #pragma once -#include -#include "obs-ffmpeg-url.h" +#include "obs-mpegts-common.h" #include #include diff --git a/plugins/obs-mpegts/obs-mpegts-srt-stats.cpp b/plugins/obs-mpegts/obs-mpegts-srt-stats.cpp new file mode 100644 index 00000000000000..ff3340d9bce861 --- /dev/null +++ b/plugins/obs-mpegts/obs-mpegts-srt-stats.cpp @@ -0,0 +1,613 @@ +/****************************************************************************** + Copyright (C) 2023 by pkv + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ +#include "obs-mpegts-srt-stats.hpp" + +#define TIMER_INTERVAL 1000 +#define QT_UTF8(str) QString::fromUtf8(str, -1) +#define QT_TO_UTF8(str) str.toUtf8().constData() + +SRTStats *_statsFrame = nullptr; + +bool WindowPositionValid(QRect rect) +{ + for (QScreen *screen : QGuiApplication::screens()) { + if (screen->availableGeometry().intersects(rect)) + return true; + } + return false; +} + +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); + } +} + +extern "C" void load_srt_stats() +{ + // initialize stats + obs_frontend_push_ui_translation(obs_module_get_string); + _statsFrame = new SRTStats(); + obs_frontend_pop_ui_translation(); + + // add the stats dock to the docks menu + if (!obs_frontend_add_dock_by_id(obs_module_text("SRT.Stats"), + obs_module_text("SRT.Stats"), + _statsFrame)) + blog(LOG_ERROR, "SRT Stats dock could not be created"); +} + +void SRTStats::OBSFrontendEvent(enum obs_frontend_event event, void *ptr) +{ + SRTStats *stats = reinterpret_cast(ptr); + + switch (event) { + case OBS_FRONTEND_EVENT_STREAMING_STARTED: + stats->chart->connect(); + break; + case OBS_FRONTEND_EVENT_STREAMING_STOPPED: + stats->chart->disconnect(); + break; + case OBS_FRONTEND_EVENT_FINISHED_LOADING: + stats->Update(); + default: + break; + } +} + +SRTStats::SRTStats() : timer(this) +{ + QVBoxLayout *mainLayout = new QVBoxLayout(); + outputLayout = new QGridLayout(); + + /* --------------------------------------------- */ + QPushButton *resetButton = + new QPushButton(obs_module_text("SRT.Stats.Reset")); + QHBoxLayout *buttonLayout = new QHBoxLayout; + buttonLayout->addStretch(); + buttonLayout->addWidget(resetButton); + + /* --------------------------------------------- */ + int col = 0; + auto addOutputCol = [&](const char *loc) { + QLabel *label = new QLabel(QT_UTF8(loc), this); + label->setStyleSheet("font-weight: bold"); + outputLayout->addWidget(label, 0, col++); + }; + + addOutputCol(obs_module_text("SRT.Stats.Status")); + addOutputCol(obs_module_text("SRT.Stats.DroppedFrames")); + addOutputCol(obs_module_text("SRT.Stats.MegabytesSent")); + addOutputCol(obs_module_text("SRT.Stats.Bitrate")); + addOutputCol(obs_module_text("SRT.Stats.RTT")); + addOutputCol(obs_module_text("SRT.Stats.Bandwidth")); + + col = 0; + auto extraAddOutputCol = [&](const char *loc) { + QLabel *label = new QLabel(QT_UTF8(loc), this); + label->setStyleSheet("font-weight: bold"); + outputLayout->addWidget(label, 2, col++); + }; + extraAddOutputCol(obs_module_text("SRT.Stats.Total.Pkts")); + extraAddOutputCol(obs_module_text("SRT.Stats.Retransmitted.Pkts")); + extraAddOutputCol(obs_module_text("SRT.Stats.Dropped.Pkts")); + extraAddOutputCol(obs_module_text("SRT.Stats.Peer.Latency")); + extraAddOutputCol(obs_module_text("SRT.Stats.Latency")); + + /* --------------------------------------------- */ + AddOutputLabels(); + /* --------------------------------------------- */ + QLabel *timeLabel = new QLabel( + QT_UTF8(obs_module_text("SRT.Stats.Time.Selector")), this); + timeLabel->setStyleSheet("font-weight: bold"); + outputLayout->addWidget(timeLabel, 4, 0); + timeBox = new QComboBox(); + outputLayout->addWidget(timeBox, 4, 1); + timeBox->addItem(QT_UTF8(obs_module_text("SRT.Stats.Time.Selector.MN")), + (int)SRT_CHART_TIME_MN); + timeBox->addItem( + QT_UTF8(obs_module_text("SRT.Stats.Time.Selector.3MN")), + (int)SRT_CHART_TIME_3MN); + timeBox->addItem( + QT_UTF8(obs_module_text("SRT.Stats.Time.Selector.5MN")), + (int)SRT_CHART_TIME_5MN); + timeBox->addItem( + QT_UTF8(obs_module_text("SRT.Stats.Time.Selector.10MN")), + (int)SRT_CHART_TIME_10MN); + timeBox->addItem(QT_UTF8(obs_module_text("SRT.Stats.Time.Selector.HR")), + (int)SRT_CHART_TIME_HR); + timeBox->addItem( + QT_UTF8(obs_module_text("SRT.Stats.Time.Selector.2HR")), + (int)SRT_CHART_TIME_2HR); + timeBox->addItem( + QT_UTF8(obs_module_text("SRT.Stats.Time.Selector.4HR")), + (int)SRT_CHART_TIME_4HR); + QObject::connect(timeBox, SIGNAL(currentIndexChanged(int)), this, + SLOT(timeBoxItemChanged(int))); + + chart = new Chart; + /* --------------------------------------------- */ + + QVBoxLayout *outputContainerLayout = new QVBoxLayout(); + outputContainerLayout->addLayout(outputLayout); + outputContainerLayout->addStretch(); + + QWidget *widget = new QWidget(this); + widget->setLayout(outputContainerLayout); + widget->setMaximumHeight(120); + + /* --------------------------------------------- */ + // add chart and chartview + QChartView *chartView = new QChartView(chart); + chartView->setRenderHint(QPainter::Antialiasing); + chartView->setMinimumSize(600, 300); + chartView->setRubberBand(QChartView::VerticalRubberBand); + chartView->setToolTip( + QString::fromUtf8(obs_module_text("SRT.Stats.Tooltip"))); + chartView->setToolTipDuration(2000); + + /* --------------------------------------------- */ + mainLayout->addWidget(widget); + mainLayout->addWidget(chartView); + mainLayout->addLayout(buttonLayout); + setLayout(mainLayout); + + /* --------------------------------------------- */ + connect(resetButton, &QPushButton::clicked, [this]() { Reset(); }); + + setWindowTitle(obs_module_text("SRT.Stats")); +#ifdef __APPLE__ + setWindowIcon( + QIcon::fromTheme("obs", QIcon(":/res/images/obs_256x256.png"))); +#else + setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs.png"))); +#endif + + setWindowModality(Qt::NonModal); + setAttribute(Qt::WA_DeleteOnClose, true); + + QObject::connect(&timer, &QTimer::timeout, this, &SRTStats::Update); + timer.setInterval(TIMER_INTERVAL); + + if (isVisible()) + timer.start(); + + config_t *conf = obs_frontend_get_global_config(); + const char *geometry = config_get_string(conf, "SRTStats", "geometry"); + if (geometry != NULL) { + QByteArray byteArray = + QByteArray::fromBase64(QByteArray(geometry)); + restoreGeometry(byteArray); + + QRect windowGeometry = normalGeometry(); + if (!WindowPositionValid(windowGeometry)) { + QRect rect = + QGuiApplication::primaryScreen()->geometry(); + setGeometry(QStyle::alignedRect(Qt::LeftToRight, + Qt::AlignCenter, size(), + rect)); + } + } + + obs_frontend_add_event_callback(OBSFrontendEvent, this); +} + +void SRTStats::closeEvent(QCloseEvent *event) +{ + config_t *conf = obs_frontend_get_global_config(); + if (isVisible()) { + config_set_string(conf, "SRTStats", "geometry", + saveGeometry().toBase64().constData()); + config_save_safe(conf, "tmp", nullptr); + } + QWidget::closeEvent(event); +} + +SRTStats::~SRTStats() +{ + obs_frontend_remove_event_callback(OBSFrontendEvent, this); +} + +void SRTStats::AddOutputLabels() +{ + outputLabels.status = new QLabel(this); + outputLabels.droppedFrames = new QLabel(this); + outputLabels.megabytesSent = new QLabel(this); + outputLabels.bitrate = new QLabel(this); + outputLabels.srt_total_pkts = new QLabel(this); + outputLabels.srt_retransmitted_pkts = new QLabel(this); + outputLabels.srt_dropped_pkts = new QLabel(this); + outputLabels.srt_rtt = new QLabel(this); + outputLabels.srt_peer_latency = new QLabel(this); + outputLabels.srt_latency = new QLabel(this); + outputLabels.srt_bandwidth = new QLabel(this); + + int col = 0; + outputLayout->addWidget(outputLabels.status, 1, col++); + outputLayout->addWidget(outputLabels.droppedFrames, 1, col++); + outputLayout->addWidget(outputLabels.megabytesSent, 1, col++); + outputLayout->addWidget(outputLabels.bitrate, 1, col++); + outputLayout->addWidget(outputLabels.srt_rtt, 1, col++); + outputLayout->addWidget(outputLabels.srt_bandwidth, 1, col++); + col = 0; + outputLayout->addWidget(outputLabels.srt_total_pkts, 3, col++); + outputLayout->addWidget(outputLabels.srt_retransmitted_pkts, 3, col++); + outputLayout->addWidget(outputLabels.srt_dropped_pkts, 3, col++); + outputLayout->addWidget(outputLabels.srt_peer_latency, 3, col++); + outputLayout->addWidget(outputLabels.srt_latency, 3, col++); +} + +void SRTStats::Update() +{ + OBSOutputAutoRelease strOutput = obs_frontend_get_streaming_output(); + if (!strOutput) + return; + /* update general stats */ + outputLabels.Update(strOutput); + /* update srt specific stats */ + outputLabels.SRTUpdate(strOutput); + chart->output = strOutput; +} + +void SRTStats::Reset() +{ + timer.start(); + OBSOutputAutoRelease strOutput = obs_frontend_get_streaming_output(); + outputLabels.Reset(strOutput); + Update(); + if (obs_frontend_streaming_active()) { + chart->disconnect(); + chart->connect(); + } +} + +void SRTStats::timeBoxItemChanged(int index) +{ + int time_mn = timeBox->itemData(index).toInt(); + chart->changeTimeSpan(time_mn); +} + +void SRTStats::OutputLabels::Update(obs_output_t *output) +{ + uint64_t totalBytes = output ? obs_output_get_total_bytes(output) : 0; + 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; + kbps = (long double)bitsBetween / timePassed / 1000.0l; + + if (timePassed < 0.01l) + kbps = 0.0l; + + QString str = obs_module_text("SRT.Stats.Status.Inactive"); + QString themeID; + bool active = output ? obs_output_active(output) : false; + + if (active) { + bool reconnecting = output ? obs_output_reconnecting(output) + : false; + + if (reconnecting) { + str = obs_module_text("SRT.Stats.Status.Reconnecting"); + themeID = "error"; + } else { + str = obs_module_text("SRT.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))); + + int total = output ? obs_output_get_total_frames(output) : 0; + int dropped = output ? obs_output_get_frames_dropped(output) : 0; + + 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 SRTStats::OutputLabels::SRTUpdate(obs_output_t *output) +{ + total_pkts = 0; + int retransmitted_pkts = 0; + int dropped_pkts = 0; + float rtt = 0; + float peer_latency = 0; + float latency = 0; + float bandwidth = 0; + + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(output); + proc_handler_call(ph, "get_srt_stats", &cd); + total_pkts = (qreal)calldata_int(&cd, "srt_total_pkts"); + retransmitted_pkts = calldata_int(&cd, "srt_retransmitted_pkts"); + dropped_pkts = calldata_int(&cd, "srt_dropped_pkts"); + rtt = calldata_float(&cd, "srt_rtt"); + peer_latency = calldata_float(&cd, "srt_peer_latency"); + latency = calldata_float(&cd, "srt_latency"); + bandwidth = calldata_float(&cd, "srt_bandwidth"); + calldata_free(&cd); + + srt_total_pkts->setText(QString::number(total_pkts).append(" pkts")); + float num = (float)retransmitted_pkts / (float)total_pkts; + QString str = QString::number(retransmitted_pkts); + str += QString(" pkts (%1%)").arg(QString::number(num, 'f', 1)); + srt_retransmitted_pkts->setText(str); + num = (float)dropped_pkts / (float)total_pkts; + str = QString::number(dropped_pkts); + str += QString(" pkts (%1%)").arg(QString::number(num, 'f', 1)); + srt_dropped_pkts->setText(str); + srt_rtt->setText(QString::number(rtt, 'f', 1).append(" ms")); + srt_peer_latency->setText( + QString::number(peer_latency, 'f', 1).append(" ms")); + srt_latency->setText(QString::number(latency, 'f', 1).append(" ms")); + srt_bandwidth->setText( + QString::number(bandwidth, 'f', 1).append(" Mbps")); +} + +void SRTStats::OutputLabels::Reset(obs_output_t *output) +{ + if (!output) + return; + + first_total = obs_output_get_total_frames(output); + first_dropped = obs_output_get_frames_dropped(output); +} + +void SRTStats::showEvent(QShowEvent *) +{ + timer.start(TIMER_INTERVAL); +} + +void SRTStats::hideEvent(QHideEvent *) +{ + timer.stop(); +} + +Chart::Chart(QGraphicsItem *parent, Qt::WindowFlags wFlags) + : QChart(QChart::ChartTypeCartesian, parent, wFlags), + retrans_pkt_series(new QSplineSeries), + dropped_pkt_series(new QSplineSeries), + rtt_series(new QSplineSeries), + dummy_series(new QLineSeries), + m_axisX(new QValueAxis()), + m_axisY(new QValueAxis()), + m_axisZ(new QValueAxis()), + m_axisT(new QDateTimeAxis()), + m_x(0), + m_y(0), + m_t(0), + old_retrans(0), + old_dropped(0), + time_span(1), + output(nullptr) +{ + QPen red(Qt::red); + QPen green(Qt::green); + QPen blue(Qt::blue); + green.setWidth(3); + red.setWidth(3); + blue.setWidth(3); + retrans_pkt_series->setPen(red); + dropped_pkt_series->setPen(green); + rtt_series->setPen(blue); + + retrans_pkt_series->setName( + obs_module_text("SRT.Stats.Retransmitted.Pkts.Rate")); + dropped_pkt_series->setName( + obs_module_text("SRT.Stats.Dropped.Pkts.Rate")); + rtt_series->setName(obs_module_text("SRT.Stats.RTT")); + + addSeries(dropped_pkt_series); + addSeries(retrans_pkt_series); + addSeries(rtt_series); + addSeries(dummy_series); + + addAxis(m_axisX, Qt::AlignTop); + addAxis(m_axisY, Qt::AlignLeft); + addAxis(m_axisZ, Qt::AlignRight); + addAxis(m_axisT, Qt::AlignBottom); + m_axisX->setVisible(false); + + retrans_pkt_series->attachAxis(m_axisX); + retrans_pkt_series->attachAxis(m_axisY); + dummy_series->attachAxis(m_axisT); + dummy_series->attachAxis(m_axisY); + dummy_series->hide(); + dropped_pkt_series->attachAxis(m_axisX); + dropped_pkt_series->attachAxis(m_axisY); + rtt_series->attachAxis(m_axisX); + rtt_series->attachAxis(m_axisZ); + + m_axisX->setTickCount(5); + m_axisX->setRange(0, 1); + m_axisX->setTitleText(obs_module_text("SRT.Stats.Time")); + m_axisY->setRange(0, 10); + m_axisY->setTitleText(obs_module_text("SRT.Stats.Packets")); + m_axisZ->setRange(0, 50); + m_axisZ->setTitleText(obs_module_text("SRT.Stats.RTT.ms")); + QBrush blueB(Qt::blue); + m_axisZ->setTitleBrush(blueB); + + m_axisT->setTickCount(5); + m_axisT->setFormat("hh:mm:ss"); + m_axisT->setTitleText(obs_module_text("SRT.Stats.Time.HMS")); + + QTime tm = QTime::currentTime(); + QTime tM = tm.addSecs(60); + QDate date = QDate::currentDate(); + QDateTime min; + min.setDate(date); + min.setTime(tm); + QDateTime max; + max.setDate(date); + max.setTime(tM); + + m_axisT->setRange(min, max); + + m_timer.start(); +} + +void Chart::connect() +{ + // Reset the chart when there's a stream starting. + m_axisX->setTickCount(5); + m_axisX->setRange(0, 60 * time_span); + m_axisY->setRange(0, 10); + m_axisZ->setRange(0, 50); + m_x = 0; + m_y = 0; + m_t = 0; + old_retrans = 0; + old_dropped = 0; + retrans_pkt_series->clear(); + dropped_pkt_series->clear(); + rtt_series->clear(); + QTime tm = QTime::currentTime(); + QTime tM = tm.addSecs(60); + QDate date = QDate::currentDate(); + QDateTime min; + min.setDate(date); + min.setTime(tm); + QDateTime max; + max.setDate(date); + max.setTime(tM); + m_axisT->setRange(min, max); + + // Refresh the chart every second. + QObject::connect(&m_timer, &QTimer::timeout, this, + &Chart::handleTimeout); + m_timer.setInterval(1000); +} + +void Chart::disconnect() +{ + QObject::disconnect(&m_timer, &QTimer::timeout, this, + &Chart::handleTimeout); +} + +void Chart::changeTimeSpan(int time_mn) +{ + time_span = time_mn; + m_axisX->setRange(m_axisX->max() - time_span * 60, m_axisX->max()); + QDateTime max = m_axisT->max(); + QDate date = QDate::currentDate(); + QDateTime min; + min.setDate(date); + QTime tm = max.time().addSecs(-60 * time_span); + min.setTime(tm); + m_axisT->setRange(min, max); +} + +Chart::~Chart() {} + +void Chart::handleTimeout() +{ + // The real width is divided in 60 slots per minute. + qreal slide_x = plotArea().width() / (60 * time_span); + qreal y = 1; + // Retrieve data from SRT output. + calldata_t cd = {0}; + proc_handler_t *ph = obs_output_get_proc_handler(output); + proc_handler_call(ph, "get_srt_stats", &cd); + qreal total_pkts = (qreal)calldata_int(&cd, "srt_total_pkts"); + UNUSED_PARAMETER( + total_pkts); // keeping it for now because we might have to use it later + qreal retransmitted_pkts = calldata_int(&cd, "srt_retransmitted_pkts"); + qreal dropped_pkts = calldata_int(&cd, "srt_dropped_pkts"); + qreal rtt = calldata_float(&cd, "srt_rtt"); + calldata_free(&cd); + + // Add retransmitted data to chart & auto-scale the axis. + m_x += y; + m_y = retransmitted_pkts - old_retrans; + old_retrans = retransmitted_pkts; + + // auto-scale the Y axis (pkts) + if (m_x == 1) + m_axisY->setRange(0, qMax(qCeil(m_y / 10) * 10, 10)); + if (m_axisY->min() < 0) + m_axisY->setRange(0, 10.0 * qCeil(m_y / 10)); + if (m_y >= 0.8 * m_axisY->max()) + m_axisY->setRange(0, qMax(2 * m_axisY->max(), + 20.0 * qCeil(m_y / 10))); + + retrans_pkt_series->append(m_x, m_y); + + // Add dropped data to chart & auto-scale the axis. + m_y = dropped_pkts - old_dropped; + old_dropped = dropped_pkts; + if (m_y >= 0.8 * m_axisY->max()) + m_axisY->setRange(0, qMax(2 * m_axisY->max(), + 20.0 * qCeil(m_y / 10))); + dropped_pkt_series->append(m_x, m_y); + + // Add RTT data to chart & auto-scale the axis. + m_y = rtt; + if (m_x == 1) + m_axisZ->setRange(0, qCeil(m_y / 10) * 10); + if (m_y >= 0.8 * m_axisZ->max()) + m_axisZ->setRange(0, qMax(2 * m_axisZ->max(), + 20.0 * qCeil(m_y / 10))); + + rtt_series->append(m_x, m_y); + + // Scroll once the whole range has been drawn + if (m_x > 60) + scroll(slide_x, 0); +} diff --git a/plugins/obs-mpegts/obs-mpegts-srt-stats.hpp b/plugins/obs-mpegts/obs-mpegts-srt-stats.hpp new file mode 100644 index 00000000000000..37c1da6751d8dd --- /dev/null +++ b/plugins/obs-mpegts/obs-mpegts-srt-stats.hpp @@ -0,0 +1,137 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +enum SRT_CHART_TIME { + SRT_CHART_TIME_MN = 1, + SRT_CHART_TIME_3MN = 3, + SRT_CHART_TIME_5MN = 5, + SRT_CHART_TIME_10MN = 10, + SRT_CHART_TIME_HR = 60, + SRT_CHART_TIME_2HR = 120, + SRT_CHART_TIME_4HR = 240 +}; + +class Chart : public QChart { + Q_OBJECT + +public: + Chart(QGraphicsItem *parent = nullptr, Qt::WindowFlags wFlags = {}); + virtual ~Chart(); + QValueAxis *m_axisX; // dummy time axis used for scrolling + QValueAxis *m_axisY; // packets per sec + QValueAxis *m_axisZ; // vertical axis for RTT + QDateTimeAxis *m_axisT; // time axis in QDateTime format + QSplineSeries *retrans_pkt_series = nullptr; + QSplineSeries *dropped_pkt_series = nullptr; + QSplineSeries *rtt_series = nullptr; + QLineSeries *dummy_series = nullptr; + obs_output_t *output; + uint time_span; // horizontal axis width in minutes + +public slots: + void handleTimeout(); + void connect(); + void disconnect(); + void changeTimeSpan(int time); + +private: + QTimer m_timer; + qreal m_x; + qreal m_y; + qreal m_t; + qreal old_retrans; + qreal old_dropped; +}; + +class SRTStats : public QFrame { + Q_OBJECT + + QGridLayout *outputLayout = nullptr; + QWidget *srtWidget = nullptr; + Chart *chart = nullptr; + QChartView *chartView = nullptr; + QComboBox *timeBox = nullptr; + QTimer timer; + uint64_t num_bytes = 0; + std::vector bitrates; + + struct OutputLabels { + QPointer status; + QPointer droppedFrames; + QPointer megabytesSent; + QPointer bitrate; + + QPointer srt_total_pkts; + QPointer srt_retransmitted_pkts; + QPointer srt_dropped_pkts; + QPointer srt_rtt; + QPointer srt_peer_latency; + QPointer srt_latency; + QPointer srt_bandwidth; + qreal total_pkts; + + /*------------------------------------------*/ + + uint64_t lastBytesSent = 0; + uint64_t lastBytesSentTime = 0; + + int first_total = 0; + int first_dropped = 0; + /* Update: general stats; SRTUpdate: SRT stats */ + void Update(obs_output_t *output); + void SRTUpdate(obs_output_t *output); + void Reset(obs_output_t *output); + + long double kbps = 0.0l; + }; + + OutputLabels outputLabels; + + void AddOutputLabels(); + void Update(); + + virtual void closeEvent(QCloseEvent *event) override; + + static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); + + void Reset(); + +public: + SRTStats(); + ~SRTStats(); + +private Q_SLOTS: + void timeBoxItemChanged(int index); + +protected: + virtual void showEvent(QShowEvent *event) override; + virtual void hideEvent(QHideEvent *event) override; +}; diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-srt.h b/plugins/obs-mpegts/obs-mpegts-srt.h similarity index 90% rename from plugins/obs-ffmpeg/obs-ffmpeg-srt.h rename to plugins/obs-mpegts/obs-mpegts-srt.h index 274ec17953545d..2293d03125338a 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-srt.h +++ b/plugins/obs-mpegts/obs-mpegts-srt.h @@ -14,12 +14,9 @@ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ - #pragma once -#include -#include "obs-ffmpeg-url.h" +#include "obs-mpegts-common.h" #include -#include #ifdef _WIN32 #include #endif @@ -37,6 +34,16 @@ enum SRTMode { SRT_MODE_RENDEZVOUS = 2 }; +struct srt_stats { + uint64_t total_packets_sent; + int packets_dropped; + int packets_retransmitted; + double rtt; + double peer_latency; + double latency; + double bandwidth; +}; + typedef struct SRTContext { SRTSOCKET fd; int eid; @@ -81,8 +88,41 @@ typedef struct SRTContext { int linger; int tsbpd; double time; // time in s in order to post logs at definite intervals + struct srt_stats *stats; } SRTContext; +static void get_srt_stats(void *data, calldata_t *cd) +{ + struct mpegts_output *output = (struct mpegts_output *)data; + SRTContext *s = (SRTContext *)output->h->priv_data; + if (s) { + if (s->stats) { + calldata_set_int(cd, "srt_total_pkts", + s->stats->total_packets_sent); + calldata_set_int(cd, "srt_dropped_pkts", + s->stats->packets_dropped); + calldata_set_int(cd, "srt_retransmitted_pkts", + s->stats->packets_retransmitted); + calldata_set_float(cd, "srt_rtt", (float)s->stats->rtt); + calldata_set_float(cd, "srt_peer_latency", + (float)s->stats->peer_latency); + calldata_set_float(cd, "srt_latency", + (float)s->stats->latency); + calldata_set_float(cd, "srt_bandwidth", + (float)s->stats->bandwidth); + } else { + calldata_set_int(cd, "srt_total_pkts", 0); + calldata_set_int(cd, "srt_dropped_pkts", 0); + calldata_set_int(cd, "srt_retransmitted_pkts", 0); + calldata_set_float(cd, "srt_rtt", 0); + calldata_set_float(cd, "srt_peer_latency", + (float)s->stats->peer_latency); + calldata_set_float(cd, "srt_latency", 0); + calldata_set_float(cd, "srt_bandwidth", 0); + } + } +} + static int libsrt_neterrno(URLContext *h) { SRTContext *s = (SRTContext *)h->priv_data; @@ -664,6 +704,7 @@ static int libsrt_open(URLContext *h, const char *uri) const char *p; char buf[1024]; int ret = 0; + s->stats = bzalloc(sizeof(struct srt_stats)); if (srt_startup() < 0) { blog(LOG_ERROR, @@ -863,13 +904,22 @@ static int libsrt_write(URLContext *h, const uint8_t *buf, int size) double time = (double)timesp.tv_sec + 0.000000001 * (double)timesp.tv_nsec; #endif - if (time > (s->time + 60.0)) { - srt_bistats(s->fd, &perf, 0, 1); + srt_bistats(s->fd, &perf, 0, 1); + s->stats->rtt = perf.msRTT; + s->stats->bandwidth = perf.mbpsBandwidth; + s->stats->peer_latency = perf.msSndTsbPdDelay; + s->stats->latency = perf.msRcvTsbPdDelay; + if (time > (s->time + 10.0)) { blog(LOG_DEBUG, - "[obs-ffmpeg mpegts muxer / libsrt]: RTT [%.2f ms], Link Bandwidth [%.1f Mbps]", - perf.msRTT, perf.mbpsBandwidth); + "[obs-ffmpeg mpegts muxer / libsrt]: RTT [%.2f ms], Link Bandwidth [%.1f Mbps], peer latency [%d ms], SRTO_RCVLATENCY [%d ms]", + perf.msRTT, perf.mbpsBandwidth, + perf.msSndTsbPdDelay, perf.msRcvTsbPdDelay); s->time = time; } + srt_bstats(s->fd, &perf, 1); + s->stats->total_packets_sent = perf.pktSentTotal; + s->stats->packets_retransmitted = perf.pktRetransTotal; + s->stats->packets_dropped = perf.pktSndDropTotal; } return ret; @@ -889,18 +939,24 @@ static int libsrt_close(URLContext *h) blog(LOG_INFO, "[obs-ffmpeg mpegts muxer / libsrt]: Session Summary\n" "\ttime elapsed [%.1f sec]\n" - "\tmean speed [%.1f Mbp]\n" - "\ttotal bytes sent [%.1f MB]\n" - "\tbytes retransmitted [%.1f %%]\n" - "\tbytes dropped [%.1f %%]\n", - (double)perf.msTimeStamp / 1000.0, perf.mbpsSendRate, - (double)perf.byteSentTotal / 1000000.0, + "\tmean speed [%.1f Mbps]\n" + "\ttotal bytes sent [%.2f MB]\n" + "\tbytes retransmitted [%.2f %%, %.2f MB]\n" + "\tbytes dropped [%.2f %%, %.2f MB]\n" + "\tpackets transmitted [%" PRId64 "]\n" + "\tpackets retransmitted [%d]\n" + "\tpackets dropped [%d]", + (double)perf.msTimeStamp / 1000.0f, perf.mbpsSendRate, + (double)perf.byteSentTotal / 1000000.0f, perf.byteSentTotal - ? perf.byteRetransTotal / perf.byteSentTotal * 100.0 - : 0, + ? (perf.byteRetransTotal * 100.0f / perf.byteSentTotal) + : 0.0f, + (double)perf.byteRetransTotal / 1000000.0f, perf.byteSentTotal - ? perf.byteSndDropTotal / perf.byteSentTotal * 100.0 - : 0); + ? perf.byteSndDropTotal / perf.byteSentTotal * 100.0f + : 0.0f, + (double)perf.byteSndDropTotal / 1000000.0f, perf.pktSentTotal, + perf.pktRetransTotal, perf.pktSndDropTotal); srt_epoll_release(s->eid); int err = srt_close(s->fd); @@ -909,7 +965,7 @@ static int libsrt_close(URLContext *h) srt_getlasterror_str()); return -1; } - + bfree(s->stats); srt_cleanup(); blog(LOG_INFO, "[obs-ffmpeg mpegts muxer / libsrt]: SRT connection closed"); diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c b/plugins/obs-mpegts/obs-mpegts.c similarity index 75% rename from plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c rename to plugins/obs-mpegts/obs-mpegts.c index ed85bd975f4678..b79ba03018b64d 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c +++ b/plugins/obs-mpegts/obs-mpegts.c @@ -15,32 +15,170 @@ along with this program. If not, see . ******************************************************************************/ -#include -#include -#include -#include -#include -#include - -#include "obs-ffmpeg-output.h" -#include "obs-ffmpeg-formats.h" -#include "obs-ffmpeg-compat.h" -#include "obs-ffmpeg-rist.h" -#include "obs-ffmpeg-srt.h" -#include -#include +#include "obs-mpegts-rist.h" +#include "obs-mpegts-srt.h" + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("obs-mpegts", "en-US") +MODULE_EXPORT const char *obs_module_description(void) +{ + return "MPEG-TS muxer to stream with srt or rist protocols"; +} /* ------------------------------------------------------------------------- */ -#define do_log(level, format, ...) \ - blog(level, "[obs-ffmpeg mpegts muxer: '%s']: " format, \ +#define do_log(level, format, ...) \ + blog(level, "[obs-mpegts muxer: '%s']: " format, \ obs_output_get_name(stream->output), ##__VA_ARGS__) #define warn(format, ...) do_log(LOG_WARNING, format, ##__VA_ARGS__) #define info(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__) #define error(format, ...) do_log(LOG_ERROR, format, ##__VA_ARGS__) -static void ffmpeg_mpegts_set_last_error(struct ffmpeg_data *data, - const char *error) +/* ------------------------------------------------------------------------- */ + +static inline int64_t rescale_ts(int64_t val, AVCodecContext *context, + AVRational new_base) +{ + return av_rescale_q_rnd(val, context->time_base, new_base, + AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); +} + +static inline enum AVPixelFormat +obs_to_mpegts_video_format(enum video_format format) +{ + switch (format) { + case VIDEO_FORMAT_I444: + return AV_PIX_FMT_YUV444P; + case VIDEO_FORMAT_I412: + return AV_PIX_FMT_YUV444P12LE; + case VIDEO_FORMAT_I420: + return AV_PIX_FMT_YUV420P; + case VIDEO_FORMAT_NV12: + return AV_PIX_FMT_NV12; + case VIDEO_FORMAT_YUY2: + return AV_PIX_FMT_YUYV422; + case VIDEO_FORMAT_UYVY: + return AV_PIX_FMT_UYVY422; + case VIDEO_FORMAT_YVYU: + return AV_PIX_FMT_YVYU422; + case VIDEO_FORMAT_RGBA: + return AV_PIX_FMT_RGBA; + case VIDEO_FORMAT_BGRA: + return AV_PIX_FMT_BGRA; + case VIDEO_FORMAT_BGRX: + return AV_PIX_FMT_BGRA; + case VIDEO_FORMAT_Y800: + return AV_PIX_FMT_GRAY8; + case VIDEO_FORMAT_BGR3: + return AV_PIX_FMT_BGR24; + case VIDEO_FORMAT_I422: + return AV_PIX_FMT_YUV422P; + case VIDEO_FORMAT_I210: + return AV_PIX_FMT_YUV422P10LE; + case VIDEO_FORMAT_I40A: + return AV_PIX_FMT_YUVA420P; + case VIDEO_FORMAT_I42A: + return AV_PIX_FMT_YUVA422P; + case VIDEO_FORMAT_YUVA: + return AV_PIX_FMT_YUVA444P; +#if LIBAVUTIL_BUILD >= AV_VERSION_INT(56, 31, 100) + case VIDEO_FORMAT_YA2L: + return AV_PIX_FMT_YUVA444P12LE; +#endif + case VIDEO_FORMAT_I010: + return AV_PIX_FMT_YUV420P10LE; + case VIDEO_FORMAT_P010: + return AV_PIX_FMT_P010LE; +#if LIBAVUTIL_BUILD >= AV_VERSION_INT(57, 17, 100) + case VIDEO_FORMAT_P216: + return AV_PIX_FMT_P216LE; + case VIDEO_FORMAT_P416: + return AV_PIX_FMT_P416LE; +#endif + case VIDEO_FORMAT_NONE: + case VIDEO_FORMAT_AYUV: + default: + return AV_PIX_FMT_NONE; + } +} + +static enum AVChromaLocation +determine_chroma_location(enum AVPixelFormat pix_fmt, + enum AVColorSpace colorspace) +{ + const AVPixFmtDescriptor *const desc = av_pix_fmt_desc_get(pix_fmt); + if (desc) { + const unsigned log_chroma_w = desc->log2_chroma_w; + const unsigned log_chroma_h = desc->log2_chroma_h; + switch (log_chroma_h) { + case 0: + switch (log_chroma_w) { + case 0: + /* 4:4:4 */ + return AVCHROMA_LOC_CENTER; + case 1: + /* 4:2:2 */ + return AVCHROMA_LOC_LEFT; + } + break; + case 1: + if (log_chroma_w == 1) { + /* 4:2:0 */ + return (colorspace == AVCOL_SPC_BT2020_NCL) + ? AVCHROMA_LOC_TOPLEFT + : AVCHROMA_LOC_LEFT; + } + } + } + + return AVCHROMA_LOC_UNSPECIFIED; +} + +static inline enum audio_format +convert_mpegts_sample_format(enum AVSampleFormat format) +{ + switch (format) { + case AV_SAMPLE_FMT_U8: + return AUDIO_FORMAT_U8BIT; + case AV_SAMPLE_FMT_S16: + return AUDIO_FORMAT_16BIT; + case AV_SAMPLE_FMT_S32: + return AUDIO_FORMAT_32BIT; + case AV_SAMPLE_FMT_FLT: + return AUDIO_FORMAT_FLOAT; + case AV_SAMPLE_FMT_U8P: + return AUDIO_FORMAT_U8BIT_PLANAR; + case AV_SAMPLE_FMT_S16P: + return AUDIO_FORMAT_16BIT_PLANAR; + case AV_SAMPLE_FMT_S32P: + return AUDIO_FORMAT_32BIT_PLANAR; + case AV_SAMPLE_FMT_FLTP: + return AUDIO_FORMAT_FLOAT_PLANAR; + default: + return AUDIO_FORMAT_16BIT; + } + + /* shouldn't get here */ + return AUDIO_FORMAT_16BIT; +} + +/* LIBAVCODEC_VERSION_CHECK checks for the right version of libav and FFmpeg + * a is the major version + * b and c the minor and micro versions of libav + * d and e the minor and micro versions of FFmpeg */ +#define LIBAVCODEC_VERSION_CHECK(a, b, c, d, e) \ + ((LIBAVCODEC_VERSION_MICRO < 100 && \ + LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(a, b, c)) || \ + (LIBAVCODEC_VERSION_MICRO >= 100 && \ + LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(a, d, e))) + +#if !LIBAVCODEC_VERSION_CHECK(54, 28, 0, 59, 100) +#define avcodec_free_frame av_freep +#endif + +/* ------------------------------------------------------------------------- */ + +static void mpegts_set_last_error(struct mpegts_data *data, const char *error) { if (data->last_error) bfree(data->last_error); @@ -48,8 +186,8 @@ static void ffmpeg_mpegts_set_last_error(struct ffmpeg_data *data, data->last_error = bstrdup(error); } -void ffmpeg_mpegts_log_error(int log_level, struct ffmpeg_data *data, - const char *format, ...) +void mpegts_log_error(int log_level, struct mpegts_data *data, + const char *format, ...) { va_list args; char out[4096]; @@ -58,24 +196,24 @@ void ffmpeg_mpegts_log_error(int log_level, struct ffmpeg_data *data, vsnprintf(out, sizeof(out), format, args); va_end(args); - ffmpeg_mpegts_set_last_error(data, out); + mpegts_set_last_error(data, out); blog(log_level, "%s", out); } -static bool is_rist(struct ffmpeg_output *stream) +static bool is_rist(struct mpegts_output *stream) { return !strncmp(stream->ff_data.config.url, RIST_PROTO, sizeof(RIST_PROTO) - 1); } -static bool is_srt(struct ffmpeg_output *stream) +static bool is_srt(struct mpegts_output *stream) { return !strncmp(stream->ff_data.config.url, SRT_PROTO, sizeof(SRT_PROTO) - 1); } -static bool proto_is_allowed(struct ffmpeg_output *stream) +static bool proto_is_allowed(struct mpegts_output *stream) { return !strncmp(stream->ff_data.config.url, UDP_PROTO, sizeof(UDP_PROTO) - 1) || @@ -85,15 +223,15 @@ static bool proto_is_allowed(struct ffmpeg_output *stream) sizeof(HTTP_PROTO) - 1); } -static bool new_stream(struct ffmpeg_data *data, AVStream **stream, +static bool new_stream(struct mpegts_data *data, AVStream **stream, const char *name) { *stream = avformat_new_stream(data->output, NULL); if (!*stream) { - ffmpeg_mpegts_log_error( - LOG_WARNING, data, - "Couldn't create stream for encoder '%s'", name); + mpegts_log_error(LOG_WARNING, data, + "Couldn't create stream for encoder '%s'", + name); return false; } @@ -101,8 +239,8 @@ static bool new_stream(struct ffmpeg_data *data, AVStream **stream, return true; } -static bool get_audio_headers(struct ffmpeg_output *stream, - struct ffmpeg_data *data, int idx) +static bool get_audio_headers(struct mpegts_output *stream, + struct mpegts_data *data, int idx) { AVCodecParameters *par = data->audio_infos[idx].stream->codecpar; obs_encoder_t *aencoder = @@ -120,8 +258,8 @@ static bool get_audio_headers(struct ffmpeg_output *stream, return 0; } -static bool get_video_headers(struct ffmpeg_output *stream, - struct ffmpeg_data *data) +static bool get_video_headers(struct mpegts_output *stream, + struct mpegts_data *data) { AVCodecParameters *par = data->video->codecpar; obs_encoder_t *vencoder = obs_output_get_video_encoder(stream->output); @@ -138,14 +276,14 @@ static bool get_video_headers(struct ffmpeg_output *stream, return 0; } -static bool create_video_stream(struct ffmpeg_output *stream, - struct ffmpeg_data *data) +static bool create_video_stream(struct mpegts_output *stream, + struct mpegts_data *data) { AVCodecContext *context; struct obs_video_info ovi; if (!obs_get_video_info(&ovi)) { - ffmpeg_mpegts_log_error(LOG_WARNING, data, "No active video"); + mpegts_log_error(LOG_WARNING, data, "No active video"); return false; } const char *name = data->config.video_encoder; @@ -241,8 +379,8 @@ static bool create_video_stream(struct ffmpeg_output *stream, return true; } -static bool create_audio_stream(struct ffmpeg_output *stream, - struct ffmpeg_data *data, int idx) +static bool create_audio_stream(struct mpegts_output *stream, + struct mpegts_data *data, int idx) { AVCodecContext *context; AVStream *avstream; @@ -257,7 +395,7 @@ static bool create_audio_stream(struct ffmpeg_output *stream, } if (!obs_get_audio_info(&aoi)) { - ffmpeg_mpegts_log_error(LOG_WARNING, data, "No active audio"); + mpegts_log_error(LOG_WARNING, data, "No active audio"); return false; } @@ -294,7 +432,7 @@ static bool create_audio_stream(struct ffmpeg_output *stream, avstream->time_base = context->time_base; data->audio_samplerate = aoi.samples_per_sec; - data->audio_format = convert_ffmpeg_sample_format(context->sample_fmt); + data->audio_format = convert_mpegts_sample_format(context->sample_fmt); data->audio_planes = get_audio_planes(data->audio_format, aoi.speakers); data->audio_size = get_audio_size(data->audio_format, aoi.speakers, 1); @@ -305,8 +443,8 @@ static bool create_audio_stream(struct ffmpeg_output *stream, return true; } -static inline bool init_streams(struct ffmpeg_output *stream, - struct ffmpeg_data *data) +static inline bool init_streams(struct mpegts_output *stream, + struct mpegts_data *data) { if (!create_video_stream(stream, data)) return false; @@ -334,20 +472,20 @@ int ff_network_init(void) return 1; } -static inline int connect_mpegts_url(struct ffmpeg_output *stream, bool is_rist) +static inline int connect_mpegts_url(struct mpegts_output *stream, bool is_rist) { int err = 0; const char *url = stream->ff_data.config.url; if (!ff_network_init()) { - ffmpeg_mpegts_log_error(LOG_ERROR, &stream->ff_data, - "Couldn't initialize network"); + mpegts_log_error(LOG_ERROR, &stream->ff_data, + "Couldn't initialize network"); return AVERROR(EIO); } URLContext *uc = av_mallocz(sizeof(URLContext) + strlen(url) + 1); if (!uc) { - ffmpeg_mpegts_log_error(LOG_ERROR, &stream->ff_data, - "Couldn't allocate memory"); + mpegts_log_error(LOG_ERROR, &stream->ff_data, + "Couldn't allocate memory"); goto fail; } uc->url = (char *)url; @@ -356,8 +494,8 @@ static inline int connect_mpegts_url(struct ffmpeg_output *stream, bool is_rist) uc->priv_data = is_rist ? av_mallocz(sizeof(RISTContext)) : av_mallocz(sizeof(SRTContext)); if (!uc->priv_data) { - ffmpeg_mpegts_log_error(LOG_ERROR, &stream->ff_data, - "Couldn't allocate memory"); + mpegts_log_error(LOG_ERROR, &stream->ff_data, + "Couldn't allocate memory"); goto fail; } /* For SRT, pass streamid & passphrase; for RIST, pass passphrase, username @@ -419,7 +557,7 @@ static inline int connect_mpegts_url(struct ffmpeg_output *stream, bool is_rist) return err; } -static inline int allocate_custom_aviocontext(struct ffmpeg_output *stream, +static inline int allocate_custom_aviocontext(struct mpegts_output *stream, bool is_rist) { /* allocate buffers */ @@ -455,8 +593,8 @@ static inline int allocate_custom_aviocontext(struct ffmpeg_output *stream, return AVERROR(ENOMEM); } -static inline int open_output_file(struct ffmpeg_output *stream, - struct ffmpeg_data *data) +static inline int open_output_file(struct mpegts_output *stream, + struct mpegts_data *data) { int ret; bool rist = is_rist(stream); @@ -471,7 +609,7 @@ static inline int open_output_file(struct ffmpeg_output *stream, if ((ret = av_dict_parse_string(&dict, data->config.protocol_settings, "=", " ", 0))) { - ffmpeg_mpegts_log_error( + mpegts_log_error( LOG_WARNING, data, "Failed to parse protocol settings: %s, %s", data->config.protocol_settings, @@ -519,10 +657,9 @@ static inline int open_output_file(struct ffmpeg_output *stream, ret == OBS_OUTPUT_INVALID_STREAM)) { error("Failed to open the url or invalid stream"); } else { - ffmpeg_mpegts_log_error(LOG_WARNING, data, - "Couldn't open '%s', %s", - data->config.url, - av_err2str(ret)); + mpegts_log_error(LOG_WARNING, data, + "Couldn't open '%s', %s", + data->config.url, av_err2str(ret)); av_dict_free(&dict); } return ret; @@ -559,12 +696,12 @@ static inline int open_output_file(struct ffmpeg_output *stream, return 0; } -static void close_video(struct ffmpeg_data *data) +static void close_video(struct mpegts_data *data) { avcodec_free_context(&data->video_ctx); } -static void close_audio(struct ffmpeg_data *data) +static void close_audio(struct mpegts_data *data) { for (int idx = 0; idx < data->num_audio_streams; idx++) { for (size_t i = 0; i < MAX_AV_PLANES; i++) @@ -580,7 +717,7 @@ static void close_audio(struct ffmpeg_data *data) } } -static void close_mpegts_url(struct ffmpeg_output *stream, bool is_rist) +static void close_mpegts_url(struct mpegts_output *stream, bool is_rist) { int err = 0; AVIOContext *s = stream->s; @@ -610,8 +747,7 @@ static void close_mpegts_url(struct ffmpeg_output *stream, bool is_rist) stream->ff_data.config.url); } -void ffmpeg_mpegts_data_free(struct ffmpeg_output *stream, - struct ffmpeg_data *data) +void mpegts_data_free(struct mpegts_output *stream, struct mpegts_data *data) { if (data->initialized) av_write_trailer(data->output); @@ -639,14 +775,13 @@ void ffmpeg_mpegts_data_free(struct ffmpeg_output *stream, if (data->last_error) bfree(data->last_error); - memset(data, 0, sizeof(struct ffmpeg_data)); + memset(data, 0, sizeof(struct mpegts_data)); } -bool ffmpeg_mpegts_data_init(struct ffmpeg_output *stream, - struct ffmpeg_data *data, - struct ffmpeg_cfg *config) +bool mpegts_data_init(struct mpegts_output *stream, struct mpegts_data *data, + struct mpegts_cfg *config) { - memset(data, 0, sizeof(struct ffmpeg_data)); + memset(data, 0, sizeof(struct mpegts_data)); data->config = *config; data->num_audio_streams = config->audio_mix_count; @@ -664,8 +799,8 @@ bool ffmpeg_mpegts_data_init(struct ffmpeg_output *stream, output_format = av_guess_format("mpegts", NULL, "video/M2PT"); if (output_format == NULL) { - ffmpeg_mpegts_log_error(LOG_WARNING, data, - "Couldn't set output format to mpegts"); + mpegts_log_error(LOG_WARNING, data, + "Couldn't set output format to mpegts"); goto fail; } else { info("Output format name and long_name: %s, %s", @@ -682,33 +817,33 @@ bool ffmpeg_mpegts_data_init(struct ffmpeg_output *stream, 0); if (!data->output) { - ffmpeg_mpegts_log_error(LOG_WARNING, data, - "Couldn't create avformat context"); + mpegts_log_error(LOG_WARNING, data, + "Couldn't create avformat context"); goto fail; } return true; fail: - warn("ffmpeg_data_init failed"); + warn("mpegts_data_init failed"); return false; } /* ------------------------------------------------------------------------- */ -static inline bool stopping(struct ffmpeg_output *output) +static inline bool stopping(struct mpegts_output *output) { return os_atomic_load_bool(&output->stopping); } -static const char *ffmpeg_mpegts_getname(void *unused) +static const char *mpegts_getname(void *unused) { UNUSED_PARAMETER(unused); return obs_module_text("FFmpegMpegts"); } -static void ffmpeg_mpegts_log_callback(void *param, int level, - const char *format, va_list args) +static void mpegts_log_callback(void *param, int level, const char *format, + va_list args) { if (level <= AV_LOG_INFO) blogva(LOG_DEBUG, format, args); @@ -716,9 +851,9 @@ static void ffmpeg_mpegts_log_callback(void *param, int level, UNUSED_PARAMETER(param); } -static void *ffmpeg_mpegts_create(obs_data_t *settings, obs_output_t *output) +static void *mpegts_create(obs_data_t *settings, obs_output_t *output) { - struct ffmpeg_output *data = bzalloc(sizeof(struct ffmpeg_output)); + struct mpegts_output *data = bzalloc(sizeof(struct mpegts_output)); pthread_mutex_init_value(&data->write_mutex); data->output = output; @@ -729,7 +864,12 @@ static void *ffmpeg_mpegts_create(obs_data_t *settings, obs_output_t *output) if (os_sem_init(&data->write_sem, 0) != 0) goto fail; - av_log_set_callback(ffmpeg_mpegts_log_callback); + av_log_set_callback(mpegts_log_callback); + proc_handler_t *ph = obs_output_get_proc_handler(output); + proc_handler_add( + ph, + "void get_srt_stats(out int srt_total_pkts, int srt_dropped_pkts, int srt_retransmitted_pkts, float srt_rtt, float srt_bandwidth)", + get_srt_stats, data); UNUSED_PARAMETER(settings); return data; @@ -741,18 +881,18 @@ static void *ffmpeg_mpegts_create(obs_data_t *settings, obs_output_t *output) return NULL; } -static void ffmpeg_mpegts_full_stop(void *data); -static void ffmpeg_mpegts_deactivate(struct ffmpeg_output *output); +static void mpegts_full_stop(void *data); +static void mpegts_deactivate(struct mpegts_output *output); -static void ffmpeg_mpegts_destroy(void *data) +static void mpegts_destroy(void *data) { - struct ffmpeg_output *output = data; + struct mpegts_output *output = data; if (output) { if (output->connecting) pthread_join(output->start_thread, NULL); - ffmpeg_mpegts_full_stop(output); + mpegts_full_stop(output); pthread_mutex_destroy(&output->write_mutex); os_sem_destroy(output->write_sem); @@ -761,10 +901,10 @@ static void ffmpeg_mpegts_destroy(void *data) } } -static uint64_t get_packet_sys_dts(struct ffmpeg_output *output, +static uint64_t get_packet_sys_dts(struct mpegts_output *output, AVPacket *packet) { - struct ffmpeg_data *data = &output->ff_data; + struct mpegts_data *data = &output->ff_data; uint64_t pause_offset = obs_output_get_pause_offset(output->output); uint64_t start_ts; @@ -783,7 +923,7 @@ static uint64_t get_packet_sys_dts(struct ffmpeg_output *output, (AVRational){1, 1000000000}); } -static int mpegts_process_packet(struct ffmpeg_output *output) +static int mpegts_process_packet(struct mpegts_output *output) { AVPacket *packet = NULL; int ret = 0; @@ -817,10 +957,9 @@ static int mpegts_process_packet(struct ffmpeg_output *output) av_freep(&buf); if (ret < 0) { - ffmpeg_mpegts_log_error( - LOG_WARNING, &output->ff_data, - "process_packet: Error writing packet: %s", - av_err2str(ret)); + mpegts_log_error(LOG_WARNING, &output->ff_data, + "process_packet: Error writing packet: %s", + av_err2str(ret)); /* Treat "Invalid data found when processing input" and * "Invalid argument" as non-fatal */ @@ -835,7 +974,7 @@ static int mpegts_process_packet(struct ffmpeg_output *output) static void *write_thread(void *data) { - struct ffmpeg_output *output = data; + struct mpegts_output *output = data; while (os_sem_wait(output->write_sem) == 0) { /* check to see if shutting down */ @@ -853,7 +992,7 @@ static void *write_thread(void *data) code = OBS_OUTPUT_NO_SPACE; obs_output_signal_stop(output->output, code); - ffmpeg_mpegts_deactivate(output); + mpegts_deactivate(output); break; } } @@ -862,9 +1001,9 @@ static void *write_thread(void *data) return NULL; } -static bool get_extradata(struct ffmpeg_output *stream) +static bool get_extradata(struct mpegts_output *stream) { - struct ffmpeg_data *ff_data = &stream->ff_data; + struct mpegts_data *ff_data = &stream->ff_data; /* get extradata for av headers from encoders */ if (!get_video_headers(stream, ff_data)) @@ -877,10 +1016,10 @@ static bool get_extradata(struct ffmpeg_output *stream) return true; } -/* set ffmpeg_config & init write_thread & capture */ -static bool set_config(struct ffmpeg_output *stream) +/* set mpegts_config & init write_thread & capture */ +static bool set_config(struct mpegts_output *stream) { - struct ffmpeg_cfg config; + struct mpegts_cfg config; bool success; int ret; int code; @@ -922,7 +1061,7 @@ static bool set_config(struct ffmpeg_output *stream) // 2.c) set video format from OBS to FFmpeg video_t *video = obs_encoder_video(vencoder); config.format = - obs_to_ffmpeg_video_format(video_output_get_format(video)); + obs_to_mpegts_video_format(video_output_get_format(video)); if (config.format == AV_PIX_FMT_NONE) { blog(LOG_WARNING, @@ -1018,17 +1157,17 @@ static bool set_config(struct ffmpeg_output *stream) config.video_settings = ""; config.audio_settings = ""; - success = ffmpeg_mpegts_data_init(stream, &stream->ff_data, &config); + success = mpegts_data_init(stream, &stream->ff_data, &config); if (!success) { if (stream->ff_data.last_error) { obs_output_set_last_error(stream->output, stream->ff_data.last_error); } - ffmpeg_mpegts_data_free(stream, &stream->ff_data); + mpegts_data_free(stream, &stream->ff_data); code = OBS_OUTPUT_INVALID_STREAM; goto fail; } - struct ffmpeg_data *ff_data = &stream->ff_data; + struct mpegts_data *ff_data = &stream->ff_data; if (!stream->got_headers) { if (!init_streams(stream, ff_data)) { error("mpegts avstream failed to be created"); @@ -1049,10 +1188,9 @@ static bool set_config(struct ffmpeg_output *stream) ret = pthread_create(&stream->write_thread, NULL, write_thread, stream); if (ret != 0) { - ffmpeg_mpegts_log_error( - LOG_WARNING, &stream->ff_data, - "ffmpeg_output_start: Failed to create write " - "thread."); + mpegts_log_error(LOG_WARNING, &stream->ff_data, + "mpegts_output_start: Failed to create write " + "thread."); code = OBS_OUTPUT_ERROR; goto fail; } @@ -1064,21 +1202,21 @@ static bool set_config(struct ffmpeg_output *stream) return true; fail: obs_output_signal_stop(stream->output, code); - ffmpeg_mpegts_full_stop(stream); + mpegts_full_stop(stream); return false; } static void *start_thread(void *data) { - struct ffmpeg_output *output = data; + struct mpegts_output *output = data; set_config(output); output->connecting = false; return NULL; } -static bool ffmpeg_mpegts_start(void *data) +static bool mpegts_start(void *data) { - struct ffmpeg_output *output = data; + struct mpegts_output *output = data; int ret; if (output->connecting) @@ -1094,19 +1232,19 @@ static bool ffmpeg_mpegts_start(void *data) return (output->connecting = (ret == 0)); } -static void ffmpeg_mpegts_full_stop(void *data) +static void mpegts_full_stop(void *data) { - struct ffmpeg_output *output = data; + struct mpegts_output *output = data; if (output->active) { obs_output_end_data_capture(output->output); - ffmpeg_mpegts_deactivate(output); + mpegts_deactivate(output); } } -static void ffmpeg_mpegts_stop(void *data, uint64_t ts) +static void mpegts_stop(void *data, uint64_t ts) { - struct ffmpeg_output *output = data; + struct mpegts_output *output = data; if (output->active) { if (ts > 0) { @@ -1114,13 +1252,13 @@ static void ffmpeg_mpegts_stop(void *data, uint64_t ts) os_atomic_set_bool(&output->stopping, true); } - ffmpeg_mpegts_full_stop(output); + mpegts_full_stop(output); } else { obs_output_signal_stop(output->output, OBS_OUTPUT_SUCCESS); } } -static void ffmpeg_mpegts_deactivate(struct ffmpeg_output *output) +static void mpegts_deactivate(struct mpegts_output *output) { if (output->write_thread_active) { os_event_signal(output->stop_event); @@ -1137,12 +1275,12 @@ static void ffmpeg_mpegts_deactivate(struct ffmpeg_output *output) pthread_mutex_unlock(&output->write_mutex); - ffmpeg_mpegts_data_free(output, &output->ff_data); + mpegts_data_free(output, &output->ff_data); } -static uint64_t ffmpeg_mpegts_total_bytes(void *data) +static uint64_t mpegts_total_bytes(void *data) { - struct ffmpeg_output *output = data; + struct mpegts_output *output = data; return output->total_bytes; } @@ -1157,7 +1295,7 @@ static inline int64_t rescale_ts2(AVStream *stream, AVRational codec_time_base, /* Convert obs encoder_packet to FFmpeg AVPacket and write to circular buffer * where it will be processed in the write_thread by process_packet. */ -void mpegts_write_packet(struct ffmpeg_output *stream, +void mpegts_write_packet(struct mpegts_output *stream, struct encoder_packet *encpacket) { if (stopping(stream) || !stream->ff_data.video || @@ -1204,17 +1342,16 @@ void mpegts_write_packet(struct ffmpeg_output *stream, av_packet_free(&packet); } -static bool write_header(struct ffmpeg_output *stream, struct ffmpeg_data *data) +static bool write_header(struct mpegts_output *stream, struct mpegts_data *data) { AVDictionary *dict = NULL; int ret; /* get mpegts muxer settings (can be used with rist, srt, rtp, etc ... */ if ((ret = av_dict_parse_string(&dict, data->config.muxer_settings, "=", " ", 0))) { - ffmpeg_mpegts_log_error( - LOG_WARNING, data, - "Failed to parse muxer settings: %s, %s", - data->config.muxer_settings, av_err2str(ret)); + mpegts_log_error(LOG_WARNING, data, + "Failed to parse muxer settings: %s, %s", + data->config.muxer_settings, av_err2str(ret)); av_dict_free(&dict); return false; @@ -1235,10 +1372,9 @@ static bool write_header(struct ffmpeg_output *stream, struct ffmpeg_data *data) /* Allocate the stream private data and write the stream header. */ ret = avformat_write_header(data->output, &dict); if (ret < 0) { - ffmpeg_mpegts_log_error( - LOG_WARNING, data, - "Error setting stream header for '%s': %s", - data->config.url, av_err2str(ret)); + mpegts_log_error(LOG_WARNING, data, + "Error setting stream header for '%s': %s", + data->config.url, av_err2str(ret)); return false; } @@ -1260,10 +1396,10 @@ static bool write_header(struct ffmpeg_output *stream, struct ffmpeg_data *data) return true; } -static void ffmpeg_mpegts_data(void *data, struct encoder_packet *packet) +static void mpegts_receive_data(void *data, struct encoder_packet *packet) { - struct ffmpeg_output *stream = data; - struct ffmpeg_data *ff_data = &stream->ff_data; + struct mpegts_output *stream = data; + struct mpegts_data *ff_data = &stream->ff_data; int code; if (!stream->got_headers) { if (get_extradata(stream)) { @@ -1288,13 +1424,13 @@ static void ffmpeg_mpegts_data(void *data, struct encoder_packet *packet) /* encoder failure */ if (!packet) { obs_output_signal_stop(stream->output, OBS_OUTPUT_ENCODE_ERROR); - ffmpeg_mpegts_deactivate(stream); + mpegts_deactivate(stream); return; } if (stopping(stream)) { if (packet->sys_dts_usec >= (int64_t)stream->stop_ts) { - ffmpeg_mpegts_deactivate(stream); + mpegts_deactivate(stream); return; } } @@ -1303,10 +1439,10 @@ static void ffmpeg_mpegts_data(void *data, struct encoder_packet *packet) return; fail: obs_output_signal_stop(stream->output, code); - ffmpeg_mpegts_full_stop(stream); + mpegts_full_stop(stream); } -static obs_properties_t *ffmpeg_mpegts_properties(void *unused) +static obs_properties_t *mpegts_properties(void *unused) { UNUSED_PARAMETER(unused); @@ -1317,7 +1453,7 @@ static obs_properties_t *ffmpeg_mpegts_properties(void *unused) return props; } -struct obs_output_info ffmpeg_mpegts_muxer = { +struct obs_output_info mpegts_muxer = { .id = "ffmpeg_mpegts_muxer", .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK | OBS_OUTPUT_SERVICE, @@ -1328,12 +1464,21 @@ struct obs_output_info ffmpeg_mpegts_muxer = { .encoded_video_codecs = "h264", #endif .encoded_audio_codecs = "aac;opus", - .get_name = ffmpeg_mpegts_getname, - .create = ffmpeg_mpegts_create, - .destroy = ffmpeg_mpegts_destroy, - .start = ffmpeg_mpegts_start, - .stop = ffmpeg_mpegts_stop, - .encoded_packet = ffmpeg_mpegts_data, - .get_total_bytes = ffmpeg_mpegts_total_bytes, - .get_properties = ffmpeg_mpegts_properties, + .get_name = mpegts_getname, + .create = mpegts_create, + .destroy = mpegts_destroy, + .start = mpegts_start, + .stop = mpegts_stop, + .encoded_packet = mpegts_receive_data, + .get_total_bytes = mpegts_total_bytes, + .get_properties = mpegts_properties, }; + +extern void load_srt_stats(void); + +bool obs_module_load(void) +{ + obs_register_output(&mpegts_muxer); + load_srt_stats(); + return true; +}