From 8b0ebd084076e79664aaa201c59e82b19caaf618 Mon Sep 17 00:00:00 2001 From: Obinna Okechukwu Date: Thu, 17 Mar 2022 14:28:41 -0400 Subject: [PATCH 1/4] UI: Add multiple services support in window-basic-main This commit changes the single OBSService used in window-basic-main into a vector. Calls to GetService() return the first entry of this vector while calls to GetServices() returns the entire vector. Calls to SetService(service) now expands the single service provided into multiple services based on urls provided in servers_url param (in service config). --- UI/api-interface.cpp | 2 +- UI/auth-oauth.cpp | 2 +- UI/streaming-helpers.cpp | 86 ++++++++++++++++++++++- UI/streaming-helpers.hpp | 7 ++ UI/window-basic-auto-config.cpp | 2 +- UI/window-basic-main-profiles.cpp | 2 +- UI/window-basic-main.cpp | 103 +++++++++++++++++++++------- UI/window-basic-main.hpp | 6 +- UI/window-basic-settings-stream.cpp | 43 +++++++++--- 9 files changed, 211 insertions(+), 42 deletions(-) diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp index dc3fa633719ca2..d6f04c4d9dff9e 100644 --- a/UI/api-interface.cpp +++ b/UI/api-interface.cpp @@ -506,7 +506,7 @@ struct OBSStudioAPI : obs_frontend_callbacks { obs_service_t *obs_frontend_get_streaming_service(void) override { - return main->GetService(); + return main->GetServices().front(); } void obs_frontend_save_streaming_service(void) override diff --git a/UI/auth-oauth.cpp b/UI/auth-oauth.cpp index c19be67bd63a1e..25b2c4a88cb35c 100644 --- a/UI/auth-oauth.cpp +++ b/UI/auth-oauth.cpp @@ -316,7 +316,7 @@ void OAuthStreamKey::OnStreamConfig() return; OBSBasic *main = OBSBasic::Get(); - obs_service_t *service = main->GetService(); + obs_service_t *service = main->GetServices().front(); OBSDataAutoRelease settings = obs_service_get_settings(service); diff --git a/UI/streaming-helpers.cpp b/UI/streaming-helpers.cpp index 24ffedc5e1049d..3fa852e4112081 100644 --- a/UI/streaming-helpers.cpp +++ b/UI/streaming-helpers.cpp @@ -7,9 +7,15 @@ #include #include #include +#include +#include +#include using namespace json11; +std::string create_server_id(const std::string &server, + const std::vector &backupServers); + static Json open_json_file(const char *path) { BPtr file_data = os_quick_read_utf8_file(path); @@ -68,6 +74,63 @@ Json get_service_from_json(const Json &root, const char *name) return Json(); } +std::vector get_backup_servers(obs_data_t *settings) +{ + OBSDataArrayAutoRelease backup_servers = + obs_data_get_array(settings, "backup_servers"); + size_t count = obs_data_array_count(backup_servers); + + std::vector result; + for (size_t i = 0; i < count; ++i) { + OBSDataAutoRelease array_obj = + obs_data_array_item(backup_servers, i); + result.push_back( + std::string(obs_data_get_string(array_obj, "server"))); + } + + return result; +} + +OBSDataArrayAutoRelease backup_servers_to_data_array(const QJsonArray &array) +{ + OBSDataArrayAutoRelease data = obs_data_array_create(); + for (auto item : array) { + OBSDataAutoRelease obj = obs_data_create(); + obs_data_set_string(obj, "server", + item.toString().toStdString().c_str()); + obs_data_array_push_back(data, obj); + } + return data; +} + +std::string create_server_id(const std::string &server, + const std::vector &backupServers) +{ + std::stringstream id; + + id << server; + for (const auto &backup_server : backupServers) + id << "," << backup_server; + + return id.str(); +} + +int find_server_index(const std::string &server, + const std::vector &backupServers, + QComboBox *serversComboBox) +{ + std::string id = create_server_id(server, backupServers); + int idx = -1; + for (int i = 0; i < serversComboBox->count(); i++) { + QJsonObject data = serversComboBox->itemData(i).toJsonObject(); + if (id == data.value("id").toString().toStdString()) { + idx = i; + break; + } + } + return idx; +} + bool StreamSettingsUI::IsServiceOutputHasNetworkFeatures() { if (IsCustomService()) @@ -199,7 +262,26 @@ void StreamSettingsUI::UpdateServerList() auto &servers = service["servers"].array_items(); for (const Json &entry : servers) { - ui_server->addItem(entry["name"].string_value().c_str(), - entry["url"].string_value().c_str()); + QJsonObject data; + QJsonArray qbackup_servers; + const auto &urls = entry["backup_urls"].array_items(); + + for (auto &url : urls) { + qbackup_servers.append( + QString::fromStdString(url.string_value())); + } + + std::vector backup_servers; + for (auto &url : urls) + backup_servers.push_back(url.string_value()); + + // `id` is used to easily lookup items in `ui_server`. + data.insert("id", QString::fromStdString(create_server_id( + entry["url"].string_value(), + backup_servers))); + data.insert("server", QString::fromStdString( + entry["url"].string_value())); + data.insert("backup_servers", qbackup_servers); + ui_server->addItem(entry["name"].string_value().c_str(), data); } } diff --git a/UI/streaming-helpers.hpp b/UI/streaming-helpers.hpp index d314d005478260..ee24aefc7fd76a 100644 --- a/UI/streaming-helpers.hpp +++ b/UI/streaming-helpers.hpp @@ -1,6 +1,7 @@ #pragma once #include "url-push-button.hpp" +#include "obs.hpp" #include #include #include @@ -10,6 +11,12 @@ extern json11::Json get_services_json(); extern json11::Json get_service_from_json(const json11::Json &root, const char *name); +extern OBSDataArrayAutoRelease +backup_servers_to_data_array(const QJsonArray &array); +extern std::vector get_backup_servers(obs_data_t *settings); +extern int find_server_index(const std::string &server, + const std::vector &backupServers, + QComboBox *serversComboBox); enum class ListOpt : int { ShowAll = 1, diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index 6976083319985c..b38c1d743dc529 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -892,7 +892,7 @@ void AutoConfig::SaveStreamSettings() const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; - obs_service_t *oldService = main->GetService(); + obs_service_t *oldService = main->GetServices().front(); OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); OBSDataAutoRelease settings = obs_data_create(); diff --git a/UI/window-basic-main-profiles.cpp b/UI/window-basic-main-profiles.cpp index 04780a142cb858..9fee36ab583bef 100644 --- a/UI/window-basic-main-profiles.cpp +++ b/UI/window-basic-main-profiles.cpp @@ -485,7 +485,7 @@ void OBSBasic::RefreshProfiles() void OBSBasic::ResetProfileData() { ResetVideo(); - service = nullptr; + services.clear(); InitService(); ResetOutputs(); ClearHotkeys(); diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 75a87a93d8d995..511f389c744834 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1198,7 +1198,7 @@ void OBSBasic::LoadData(obs_data_t *data, const char *file) void OBSBasic::SaveService() { - if (!service) + if (services.empty()) return; char serviceJsonPath[512]; @@ -1207,6 +1207,7 @@ void OBSBasic::SaveService() if (ret <= 0) return; + obs_service_t *service = GetServices().front(); OBSDataAutoRelease data = obs_data_create(); OBSDataAutoRelease settings = obs_service_get_settings(service); @@ -1239,11 +1240,54 @@ bool OBSBasic::LoadService() OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - service = obs_service_create(type, "default_service", settings, - hotkey_data); - obs_service_release(service); + OBSServiceAutoRelease service = obs_service_create( + type, "default_service", settings, hotkey_data); + services = ExpandService(service); + + return !services.empty(); +} + +std::vector OBSBasic::ExpandService(obs_service_t *service) +{ + std::vector services; + if (!service) { + return services; + } + + services.push_back(service); + + // If service contains no backup servers, return original service. + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *srv = obs_data_get_string(settings, "server"); + OBSDataArrayAutoRelease backup_servers = + obs_data_get_array(settings, "backup_servers"); + size_t backup_count = obs_data_array_count(backup_servers); + + if (backup_count == 0) { + return services; + } + + // If service contains one or more backup servers, expand it. + const char *name = obs_service_get_name(service); + const char *type = obs_service_get_type(service); + OBSDataAutoRelease hotkeys = obs_hotkeys_save_service(service); + + for (size_t i = 0; i < backup_count; ++i) { + OBSDataAutoRelease array_obj = + obs_data_array_item(backup_servers, i); + const char *bk_server = + obs_data_get_string(array_obj, "server"); - return !!service; + OBSDataAutoRelease settings_copy = obs_data_create(); + obs_data_apply(settings_copy, settings); + obs_data_set_string(settings_copy, "server", bk_server); + + OBSService expanded = + obs_service_create(type, name, settings_copy, hotkeys); + services.push_back(expanded); + obs_service_release(expanded); + } + return services; } bool OBSBasic::InitService() @@ -1253,10 +1297,12 @@ bool OBSBasic::InitService() if (LoadService()) return true; - service = obs_service_create("rtmp_common", "default_service", nullptr, - nullptr); + OBSService service = obs_service_create( + "rtmp_common", "default_service", nullptr, nullptr); if (!service) return false; + + services.push_back(service); obs_service_release(service); return true; @@ -2617,7 +2663,7 @@ OBSBasic::~OBSBasic() obs_hotkey_set_callback_routing_func(nullptr, nullptr); ClearHotkeys(); - service = nullptr; + services.clear(); outputHandler.reset(); if (interaction) @@ -4225,20 +4271,21 @@ void OBSBasic::RenderMain(void *data, uint32_t cx, uint32_t cy) /* Main class functions */ -obs_service_t *OBSBasic::GetService() +std::vector OBSBasic::GetServices() { - if (!service) { - service = + if (services.empty()) { + OBSService service = obs_service_create("rtmp_common", NULL, NULL, nullptr); obs_service_release(service); + services.push_back(service); } - return service; + return services; } void OBSBasic::SetService(obs_service_t *newService) { if (newService) { - service = newService; + services = ExpandService(newService); } } @@ -6348,16 +6395,21 @@ void OBSBasic::YouTubeActionDialogOk(const QString &id, const QString &key, bool start_now) { //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + auto servs = GetServices(); + for (auto &service : servs) { + obs_service_t *service_obj = service.Get(); + OBSDataAutoRelease settings = + obs_service_get_settings(service_obj); - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); + const std::string a_key = QT_TO_UTF8(key); + obs_data_set_string(settings, "key", a_key.c_str()); - const std::string an_id = QT_TO_UTF8(id); - obs_data_set_string(settings, "stream_id", an_id.c_str()); + const std::string an_id = QT_TO_UTF8(id); + obs_data_set_string(settings, "stream_id", an_id.c_str()); + + obs_service_update(service_obj, settings); + } - obs_service_update(service_obj, settings); autoStartBroadcast = autostart; autoStopBroadcast = autostop; broadcastReady = true; @@ -6480,7 +6532,7 @@ void OBSBasic::StartStreaming() } } - if (!outputHandler->SetupStreaming(service)) { + if (!outputHandler->SetupStreaming(services.front())) { DisplayStreamStartError(); return; } @@ -6500,7 +6552,7 @@ void OBSBasic::StartStreaming() sysTrayStream->setText(ui->streamButton->text()); } - if (!outputHandler->StartStreaming(service)) { + if (!outputHandler->StartStreaming(services.front())) { DisplayStreamStartError(); return; } @@ -6910,7 +6962,9 @@ void OBSBasic::StreamingStart() ui->streamButton->setText(QTStr("Basic.Main.StopStreaming")); ui->streamButton->setEnabled(true); ui->streamButton->setChecked(true); - ui->statusbar->StreamStarted(outputHandler->streamOutput); + if (!outputHandler->streamOutput) { + ui->statusbar->StreamStarted(outputHandler->streamOutput); + } if (sysTrayStream) { sysTrayStream->setText(ui->streamButton->text()); @@ -6920,7 +6974,7 @@ void OBSBasic::StreamingStart() #if YOUTUBE_ENABLED if (!autoStartBroadcast) { // get a current stream key - obs_service_t *service_obj = GetService(); + obs_service_t *service_obj = GetServices().front(); OBSDataAutoRelease settings = obs_service_get_settings(service_obj); std::string key = obs_data_get_string(settings, "stream_id"); @@ -7545,6 +7599,7 @@ void OBSBasic::on_streamButton_clicked() } Auth *auth = GetAuth(); + obs_service_t *service = GetServices().front(); auto action = (auth && auth->external()) diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 24660492bec3c7..116f97e0af17cf 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -253,7 +253,7 @@ class OBSBasic : public OBSMainWindow { os_cpu_usage_info_t *cpuUsageInfo = nullptr; - OBSService service; + std::vector services; std::unique_ptr outputHandler; bool streamingStopping = false; bool recordingStopping = false; @@ -829,7 +829,8 @@ private slots: return OBSSource(obs_scene_get_source(curScene)); } - obs_service_t *GetService(); + std::vector GetServices(); + void SetService(obs_service_t *service); int GetTransitionDuration(); @@ -874,6 +875,7 @@ private slots: void SaveService(); bool LoadService(); + std::vector ExpandService(obs_service_t *service); inline Auth *GetAuth() { return auth.get(); } diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index bbc97dc3b1a5e4..6d9749a9f15d53 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include "window-basic-settings.hpp" #include "obs-frontend-api.h" @@ -104,7 +106,7 @@ void OBSBasicSettings::LoadStream1Settings() bool ignoreRecommended = config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); - obs_service_t *service_obj = main->GetService(); + obs_service_t *service_obj = main->GetServices().front(); const char *type = obs_service_get_type(service_obj); loading = true; @@ -112,12 +114,13 @@ void OBSBasicSettings::LoadStream1Settings() OBSDataAutoRelease settings = obs_service_get_settings(service_obj); const char *service = obs_data_get_string(settings, "service"); - const char *server = obs_data_get_string(settings, "server"); + std::string server = + std::string(obs_data_get_string(settings, "server")); const char *key = obs_data_get_string(settings, "key"); if (strcmp(type, "rtmp_custom") == 0) { ui->service->setCurrentIndex(0); - ui->customServer->setText(server); + ui->customServer->setText(server.c_str()); bool use_auth = obs_data_get_bool(settings, "use_auth"); const char *username = @@ -146,10 +149,16 @@ void OBSBasicSettings::LoadStream1Settings() streamUi.UpdateServerList(); if (strcmp(type, "rtmp_common") == 0) { - int idx = ui->server->findData(server); + std::vector backups = get_backup_servers(settings); + int idx = find_server_index(server, backups, ui->server); + if (idx == -1) { - if (server && *server) - ui->server->insertItem(0, server, server); + QJsonObject data; + data.insert("id", QString::fromStdString(server)); + data.insert("server", QString::fromStdString(server)); + + if (!server.empty()) + ui->server->insertItem(0, server.c_str(), data); idx = 0; } ui->server->setCurrentIndex(idx); @@ -181,7 +190,7 @@ void OBSBasicSettings::SaveStream1Settings() bool customServer = streamUi.IsCustomService(); const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; - obs_service_t *oldService = main->GetService(); + obs_service_t *oldService = main->GetServices().front(); OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); OBSDataAutoRelease settings = obs_data_create(); @@ -189,9 +198,16 @@ void OBSBasicSettings::SaveStream1Settings() if (!customServer) { obs_data_set_string(settings, "service", QT_TO_UTF8(ui->service->currentText())); + QJsonObject data = ui->server->currentData().toJsonObject(); + obs_data_set_string( settings, "server", - QT_TO_UTF8(ui->server->currentData().toString())); + QT_TO_UTF8(data.value("server").toString())); + + obs_data_set_array( + settings, "backup_servers", + backup_servers_to_data_array( + data.value("backup_servers").toArray())); } else { obs_data_set_string( settings, "server", @@ -400,9 +416,16 @@ OBSService OBSBasicSettings::SpawnTempService() if (!custom) { obs_data_set_string(settings, "service", QT_TO_UTF8(ui->service->currentText())); + QJsonObject data = ui->server->currentData().toJsonObject(); + obs_data_set_string( settings, "server", - QT_TO_UTF8(ui->server->currentData().toString())); + QT_TO_UTF8(data.value("server").toString())); + + obs_data_set_array( + settings, "backup_servers", + backup_servers_to_data_array( + data.value("backup_servers").toArray())); } else { obs_data_set_string( settings, "server", @@ -635,7 +658,7 @@ void OBSBasicSettings::UpdateVodTrackSetting() OBSService OBSBasicSettings::GetStream1Service() { return stream1Changed ? SpawnTempService() - : OBSService(main->GetService()); + : OBSService(main->GetServices().front()); } void OBSBasicSettings::UpdateServiceRecommendations() From ee7d9f22ffc86e28cccf294a9676c4783f918b10 Mon Sep 17 00:00:00 2001 From: Obinna Okechukwu Date: Thu, 17 Mar 2022 15:33:31 -0400 Subject: [PATCH 2/4] UI: Add multiple stream output support This commit changes the single streamOutput of type OBSOutputAutoRelease to a vector of stream outputs. The StartStream() and SetupStream() calls were modified to now accept a list of services. External usages of streamOutput (such as api calls) will get the first entry in the streamOutputs vector. Co-authored-by: Samuel Zeleke --- UI/api-interface.cpp | 5 +- UI/window-basic-main-outputs.cpp | 560 +++++++++++++++++++------------ UI/window-basic-main-outputs.hpp | 8 +- UI/window-basic-main.cpp | 15 +- UI/window-basic-status-bar.cpp | 5 +- 5 files changed, 360 insertions(+), 233 deletions(-) diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp index d6f04c4d9dff9e..b19710d368307e 100644 --- a/UI/api-interface.cpp +++ b/UI/api-interface.cpp @@ -385,7 +385,10 @@ struct OBSStudioAPI : obs_frontend_callbacks { obs_output_t *obs_frontend_get_streaming_output(void) override { - OBSOutput output = main->outputHandler->streamOutput.Get(); + if (main->outputHandler->streamOutputs.empty()) + return nullptr; + + OBSOutput output = main->outputHandler->streamOutputs[0].Get(); return obs_output_get_ref(output); } diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index 0035e7ca99597a..95dccdff6c7e48 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -212,6 +212,24 @@ static bool CreateAACEncoder(OBSEncoder &res, string &id, int bitrate, return false; } +static std::string GetServiceOutputType(const obs_service_t *service) +{ + const char *type = obs_service_get_output_type(service); + if (!type) { + type = "rtmp_output"; + const char *url = obs_service_get_url(service); + if (url != NULL && + strncmp(url, FTL_PROTOCOL, strlen(FTL_PROTOCOL)) == 0) { + type = "ftl_output"; + } else if (url != NULL && strncmp(url, RTMP_PROTOCOL, + strlen(RTMP_PROTOCOL)) != 0) { + type = "ffmpeg_mpegts_muxer"; + } + } + + return {type}; +} + /* ------------------------------------------------------------------------ */ inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) @@ -305,10 +323,12 @@ struct SimpleOutput : BasicOutputHandler { void UpdateRecording(); bool ConfigureRecording(bool useReplayBuffer); - void SetupVodTrack(obs_service_t *service); + void SetupVodTrack(obs_service_t *service, obs_output_t *output); - virtual bool SetupStreaming(obs_service_t *service) override; - virtual bool StartStreaming(obs_service_t *service) override; + virtual bool + SetupStreaming(const std::vector &services) override; + virtual bool + StartStreaming(const std::vector &services) override; virtual bool StartRecording() override; virtual bool StartReplayBuffer() override; virtual void StopStreaming(bool force) override; @@ -558,8 +578,10 @@ void SimpleOutput::Update() obs_data_set_string(aacSettings, "rate_control", "CBR"); obs_data_set_int(aacSettings, "bitrate", audioBitrate); - obs_service_apply_encoder_settings(main->GetService(), videoSettings, - aacSettings); + for (auto &service : main->GetServices()) { + obs_service_apply_encoder_settings(service, videoSettings, + aacSettings); + } if (!enforceBitrate) { obs_data_set_int(videoSettings, "bitrate", videoBitrate); @@ -791,7 +813,7 @@ const char *FindAudioEncoderFromCodec(const char *type) return nullptr; } -bool SimpleOutput::SetupStreaming(obs_service_t *service) +bool SimpleOutput::SetupStreaming(const std::vector &services) { if (!Active()) SetupOutputs(); @@ -801,90 +823,94 @@ bool SimpleOutput::SetupStreaming(obs_service_t *service) auth->OnStreamConfig(); /* --------------------- */ + bool outputsCleared = false; + const std::string type = GetServiceOutputType(services.front()); - const char *type = obs_service_get_output_type(service); - if (!type) { - type = "rtmp_output"; - const char *url = obs_service_get_url(service); - if (url != NULL && - strncmp(url, FTL_PROTOCOL, strlen(FTL_PROTOCOL)) == 0) { - type = "ftl_output"; - } else if (url != NULL && strncmp(url, RTMP_PROTOCOL, - strlen(RTMP_PROTOCOL)) != 0) { - type = "ffmpeg_mpegts_muxer"; - } - } - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { + if (outputType != type || streamOutputs.size() != services.size()) { streamDelayStarting.Disconnect(); streamStopping.Disconnect(); startStreaming.Disconnect(); stopStreaming.Disconnect(); + streamOutputs.clear(); + outputsCleared = true; + } - streamOutput = obs_output_create(type, "simple_stream", nullptr, - nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect( - obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.Connect( - obs_output_get_signal_handler(streamOutput), "stopping", - OBSStreamStopping, this); - - startStreaming.Connect( - obs_output_get_signal_handler(streamOutput), "start", - OBSStartStreaming, this); - stopStreaming.Connect( - obs_output_get_signal_handler(streamOutput), "stop", - OBSStopStreaming, this); - - bool isEncoded = obs_output_get_flags(streamOutput) & - OBS_OUTPUT_ENCODED; - - if (isEncoded) { - const char *codec = - obs_output_get_supported_audio_codecs( - streamOutput); - if (!codec) { - blog(LOG_WARNING, "Failed to load audio codec"); + for (auto &service : services) { + /* XXX: this is messy and disgusting and should be refactored */ + if (outputsCleared) { + OBSOutputAutoRelease output = + obs_output_create(type.c_str(), "simple_stream", + nullptr, nullptr); + if (!output) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type.c_str()); return false; } - if (strcmp(codec, "aac") != 0) { - const char *id = - FindAudioEncoderFromCodec(codec); - int audioBitrate = GetAudioBitrate(); - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_int(settings, "bitrate", - audioBitrate); - - aacStreaming = obs_audio_encoder_create( - id, "alt_audio_enc", nullptr, 0, - nullptr); - obs_encoder_release(aacStreaming); - if (!aacStreaming) + streamDelayStarting.Connect( + obs_output_get_signal_handler(output), + "starting", OBSStreamStarting, this); + streamStopping.Connect( + obs_output_get_signal_handler(output), + "stopping", OBSStreamStopping, this); + + startStreaming.Connect( + obs_output_get_signal_handler(output), "start", + OBSStartStreaming, this); + stopStreaming.Connect( + obs_output_get_signal_handler(output), "stop", + OBSStopStreaming, this); + + bool isEncoded = obs_output_get_flags(output) & + OBS_OUTPUT_ENCODED; + + if (isEncoded) { + const char *codec = + obs_output_get_supported_audio_codecs( + output); + if (!codec) { + blog(LOG_WARNING, + "Failed to load audio codec"); return false; - - obs_encoder_update(aacStreaming, settings); - obs_encoder_set_audio(aacStreaming, - obs_get_audio()); + } + + if (strcmp(codec, "aac") != 0) { + const char *id = + FindAudioEncoderFromCodec( + codec); + int audioBitrate = GetAudioBitrate(); + OBSDataAutoRelease settings = + obs_data_create(); + obs_data_set_int(settings, "bitrate", + audioBitrate); + + aacStreaming = obs_audio_encoder_create( + id, "alt_audio_enc", nullptr, 0, + nullptr); + obs_encoder_release(aacStreaming); + if (!aacStreaming) + return false; + + obs_encoder_update(aacStreaming, + settings); + obs_encoder_set_audio(aacStreaming, + obs_get_audio()); + } } + + obs_output_set_video_encoder(output, videoStreaming); + obs_output_set_audio_encoder(output, aacStreaming, 0); + obs_output_set_service(output, service); + streamOutputs.push_back(std::move(output)); } + } + if (outputType != type) { outputType = type; } - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, aacStreaming, 0); - obs_output_set_service(streamOutput, service); return true; } @@ -907,7 +933,7 @@ static void clear_archive_encoder(obs_output_t *output, obs_output_set_audio_encoder(output, nullptr, 1); } -void SimpleOutput::SetupVodTrack(obs_service_t *service) +void SimpleOutput::SetupVodTrack(obs_service_t *service, obs_output_t *output) { bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); @@ -926,12 +952,12 @@ void SimpleOutput::SetupVodTrack(obs_service_t *service) enable = advanced && enable && ServiceSupportsVodTrack(name); if (enable) - obs_output_set_audio_encoder(streamOutput, aacArchive, 1); + obs_output_set_audio_encoder(output, aacArchive, 1); else - clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); + clear_archive_encoder(output, SIMPLE_ARCHIVE_NAME); } -bool SimpleOutput::StartStreaming(obs_service_t *service) +bool SimpleOutput::StartStreaming(const std::vector &services) { bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); int retryDelay = @@ -959,33 +985,53 @@ bool SimpleOutput::StartStreaming(obs_service_t *service) obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - obs_output_update(streamOutput, settings); - if (!reconnect) - maxRetries = 0; + if (services.size() != streamOutputs.size()) { + blog(LOG_ERROR, + "Stream failed to start because number of streaming outputs and services don't match."); + return false; + } + + int errorCount = 0; + for (int i = 0; i < (int)services.size(); i++) { + obs_service_t *service = services[i]; + obs_output_t *output = streamOutputs[i]; - obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, - preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + obs_output_update(output, settings); - obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + if (!reconnect) + maxRetries = 0; - SetupVodTrack(service); + obs_output_set_delay(output, useDelay ? delaySec : 0, + preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE + : 0); - if (obs_output_start(streamOutput)) { - return true; - } + obs_output_set_reconnect_settings(output, maxRetries, + retryDelay); - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); + SetupVodTrack(service, output); - const char *type = obs_service_get_output_type(service); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, - hasLastError ? " Last Error: " : "", hasLastError ? error : ""); - return false; + const bool started = obs_output_start(output); + if (started) { + continue; + } + + ++errorCount; + const char *error = obs_output_get_last_error(output); + bool hasLastError = error && *error; + if (hasLastError) + lastError = error; + else + lastError = string(); + + const char *type = obs_service_get_output_type(service); + blog(LOG_WARNING, + "Stream output type '%s' failed to start!%s%s", type, + hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + } + + return errorCount == 0; } void SimpleOutput::UpdateRecording() @@ -993,10 +1039,19 @@ void SimpleOutput::UpdateRecording() if (replayBufferActive || recordingActive) return; + bool streamOutputsActive = true; + + for (auto &output : streamOutputs) { + if (!obs_output_active(output)) { + streamOutputsActive = false; + break; + } + } + if (usingRecordingPreset) { if (!ffmpegOutput) UpdateRecordingSettings(); - } else if (!obs_output_active(streamOutput)) { + } else if (!streamOutputsActive) { Update(); } @@ -1111,10 +1166,12 @@ bool SimpleOutput::StartReplayBuffer() void SimpleOutput::StopStreaming(bool force) { - if (force) - obs_output_force_stop(streamOutput); - else - obs_output_stop(streamOutput); + for (auto &output : streamOutputs) { + if (force) + obs_output_force_stop(output); + else + obs_output_stop(output); + } } void SimpleOutput::StopRecording(bool force) @@ -1135,7 +1192,11 @@ void SimpleOutput::StopReplayBuffer(bool force) bool SimpleOutput::StreamingActive() const { - return obs_output_active(streamOutput); + for (auto &output : streamOutputs) { + if (obs_output_active(output)) + return true; + } + return false; } bool SimpleOutput::RecordingActive() const @@ -1171,7 +1232,7 @@ struct AdvancedOutput : BasicOutputHandler { inline void UpdateAudioSettings(); virtual void Update() override; - inline void SetupVodTrack(obs_service_t *service); + inline void SetupVodTrack(obs_service_t *service, obs_output_t *output); inline void SetupStreaming(); inline void SetupRecording(); @@ -1179,8 +1240,10 @@ struct AdvancedOutput : BasicOutputHandler { void SetupOutputs() override; int GetAudioBitrate(size_t i) const; - virtual bool SetupStreaming(obs_service_t *service) override; - virtual bool StartStreaming(obs_service_t *service) override; + virtual bool + SetupStreaming(const std::vector &services) override; + virtual bool + StartStreaming(const std::vector &services) override; virtual bool StartRecording() override; virtual bool StartReplayBuffer() override; virtual void StopStreaming(bool force) override; @@ -1383,8 +1446,11 @@ void AdvancedOutput::UpdateStreamSettings() if (applyServiceSettings) { int bitrate = (int)obs_data_get_int(settings, "bitrate"); int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - obs_service_apply_encoder_settings(main->GetService(), settings, - nullptr); + for (auto &service : main->GetServices()) { + obs_service_apply_encoder_settings(service, settings, + nullptr); + } + if (!enforceBitrate) obs_data_set_int(settings, "bitrate", bitrate); @@ -1455,15 +1521,21 @@ inline void AdvancedOutput::SetupStreaming() } } - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); + for (auto &output : streamOutputs) { + obs_output_set_audio_encoder(output, streamAudioEnc, 0); + } + obs_encoder_set_scaled_size(videoStreaming, cx, cy); obs_encoder_set_video(videoStreaming, obs_get_video()); - const char *id = obs_service_get_id(main->GetService()); + auto services = main->GetServices(); + const char *id = obs_service_get_id(services.front()); if (strcmp(id, "rtmp_custom") == 0) { OBSDataAutoRelease settings = obs_data_create(); - obs_service_apply_encoder_settings(main->GetService(), settings, - nullptr); + for (auto &service : services) { + obs_service_apply_encoder_settings(service, settings, + nullptr); + } obs_encoder_update(videoStreaming, settings); } } @@ -1649,9 +1721,10 @@ inline void AdvancedOutput::UpdateAudioSettings() if (applyServiceSettings) { int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); - obs_service_apply_encoder_settings( - main->GetService(), nullptr, - settings[i]); + for (auto &service : main->GetServices()) { + obs_service_apply_encoder_settings( + service, nullptr, settings[i]); + } if (!enforceBitrate) obs_data_set_int(settings[i], "bitrate", @@ -1694,7 +1767,8 @@ int AdvancedOutput::GetAudioBitrate(size_t i) const return FindClosestAvailableAACBitrate(bitrate); } -inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service, + obs_output_t *output) { int streamTrack = config_get_int(main->Config(), "AdvOut", "TrackIndex"); @@ -1717,12 +1791,12 @@ inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) } if (vodTrackEnabled && streamTrack != vodTrackIndex) - obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); + obs_output_set_audio_encoder(output, streamArchiveEnc, 1); else - clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); + clear_archive_encoder(output, ADV_ARCHIVE_NAME); } -bool AdvancedOutput::SetupStreaming(obs_service_t *service) +bool AdvancedOutput::SetupStreaming(const std::vector &services) { int streamTrack = config_get_int(main->Config(), "AdvOut", "TrackIndex"); @@ -1742,98 +1816,102 @@ bool AdvancedOutput::SetupStreaming(obs_service_t *service) auth->OnStreamConfig(); /* --------------------- */ + bool outputsCleared = false; + const std::string type = GetServiceOutputType(services.front()); - const char *type = obs_service_get_output_type(service); - if (!type) { - type = "rtmp_output"; - const char *url = obs_service_get_url(service); - if (url != NULL && - strncmp(url, FTL_PROTOCOL, strlen(FTL_PROTOCOL)) == 0) { - type = "ftl_output"; - } else if (url != NULL && strncmp(url, RTMP_PROTOCOL, - strlen(RTMP_PROTOCOL)) != 0) { - type = "ffmpeg_mpegts_muxer"; - } - } - - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { + if (outputType != type || streamOutputs.size() != services.size()) { streamDelayStarting.Disconnect(); streamStopping.Disconnect(); startStreaming.Disconnect(); stopStreaming.Disconnect(); + streamOutputs.clear(); + outputsCleared = true; + } - streamOutput = - obs_output_create(type, "adv_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } - - streamDelayStarting.Connect( - obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.Connect( - obs_output_get_signal_handler(streamOutput), "stopping", - OBSStreamStopping, this); - - startStreaming.Connect( - obs_output_get_signal_handler(streamOutput), "start", - OBSStartStreaming, this); - stopStreaming.Connect( - obs_output_get_signal_handler(streamOutput), "stop", - OBSStopStreaming, this); - - bool isEncoded = obs_output_get_flags(streamOutput) & - OBS_OUTPUT_ENCODED; - - if (isEncoded) { - const char *codec = - obs_output_get_supported_audio_codecs( - streamOutput); - if (!codec) { - blog(LOG_WARNING, "Failed to load audio codec"); + for (auto &service : services) { + /* XXX: this is messy and disgusting and should be refactored */ + if (outputsCleared) { + OBSOutputAutoRelease output = obs_output_create( + type.c_str(), "adv_stream", nullptr, nullptr); + if (!output) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type.c_str()); return false; } - if (strcmp(codec, "aac") != 0) { - OBSDataAutoRelease settings = - obs_encoder_get_settings( - streamAudioEnc); - - const char *id = - FindAudioEncoderFromCodec(codec); - - streamAudioEnc = obs_audio_encoder_create( - id, "alt_audio_enc", nullptr, - streamTrack - 1, nullptr); - - if (!streamAudioEnc) + streamDelayStarting.Connect( + obs_output_get_signal_handler(output), + "starting", OBSStreamStarting, this); + streamStopping.Connect( + obs_output_get_signal_handler(output), + "stopping", OBSStreamStopping, this); + + startStreaming.Connect( + obs_output_get_signal_handler(output), "start", + OBSStartStreaming, this); + stopStreaming.Connect( + obs_output_get_signal_handler(output), "stop", + OBSStopStreaming, this); + + bool isEncoded = obs_output_get_flags(output) & + OBS_OUTPUT_ENCODED; + + if (isEncoded) { + const char *codec = + obs_output_get_supported_audio_codecs( + output); + if (!codec) { + blog(LOG_WARNING, + "Failed to load audio codec"); return false; - - obs_encoder_release(streamAudioEnc); - obs_encoder_update(streamAudioEnc, settings); - obs_encoder_set_audio(streamAudioEnc, - obs_get_audio()); + } + + if (strcmp(codec, "aac") != 0) { + OBSDataAutoRelease settings = + obs_encoder_get_settings( + streamAudioEnc); + + const char *id = + FindAudioEncoderFromCodec( + codec); + + streamAudioEnc = + obs_audio_encoder_create( + id, "alt_audio_enc", + nullptr, + streamTrack - 1, + nullptr); + + if (!streamAudioEnc) + return false; + + obs_encoder_release(streamAudioEnc); + obs_encoder_update(streamAudioEnc, + settings); + obs_encoder_set_audio(streamAudioEnc, + obs_get_audio()); + } } + + obs_output_set_video_encoder(output, videoStreaming); + obs_output_set_audio_encoder(output, streamAudioEnc, 0); + obs_output_set_service(output, service); + + streamOutputs.push_back(std::move(output)); } + } + if (outputType != type) { outputType = type; } - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - return true; } -bool AdvancedOutput::StartStreaming(obs_service_t *service) +bool AdvancedOutput::StartStreaming(const std::vector &services) { - obs_output_set_service(streamOutput, service); - bool reconnect = config_get_bool(main->Config(), "Output", "Reconnect"); int retryDelay = config_get_int(main->Config(), "Output", "RetryDelay"); int maxRetries = config_get_int(main->Config(), "Output", "MaxRetries"); @@ -1858,33 +1936,55 @@ bool AdvancedOutput::StartStreaming(obs_service_t *service) obs_data_set_bool(settings, "low_latency_mode_enabled", enableLowLatencyMode); obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); - obs_output_update(streamOutput, settings); - if (!reconnect) - maxRetries = 0; + if (services.size() != streamOutputs.size()) { + blog(LOG_ERROR, + "Stream failed to start because number of streaming outputs and services don't match."); + return false; + } - obs_output_set_delay(streamOutput, useDelay ? delaySec : 0, - preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); + int errorCount = 0; + for (int i = 0; i < (int)services.size(); i++) { + obs_service_t *service = services[i]; + obs_output_t *output = streamOutputs[i]; - obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); + obs_output_update(output, settings); - SetupVodTrack(service); + if (!reconnect) + maxRetries = 0; - if (obs_output_start(streamOutput)) { - return true; - } + obs_output_set_delay(output, useDelay ? delaySec : 0, + preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE + : 0); - const char *error = obs_output_get_last_error(streamOutput); - bool hasLastError = error && *error; - if (hasLastError) - lastError = error; - else - lastError = string(); + obs_output_set_reconnect_settings(output, maxRetries, + retryDelay); - const char *type = obs_service_get_output_type(service); - blog(LOG_WARNING, "Stream output type '%s' failed to start!%s%s", type, - hasLastError ? " Last Error: " : "", hasLastError ? error : ""); - return false; + SetupVodTrack(service, output); + + const bool started = obs_output_start(output); + if (started) { + continue; + } + + ++errorCount; + const char *error = obs_output_get_last_error(output); + bool hasLastError = error && *error; + if (hasLastError) { + lastError = error; + ++errorCount; + } else { + lastError = string(); + } + + const char *type = obs_service_get_output_type(service); + blog(LOG_WARNING, + "Stream output type '%s' failed to start!%s%s", type, + hasLastError ? " Last Error: " : "", + hasLastError ? error : ""); + } + + return errorCount == 0; } bool AdvancedOutput::StartRecording() @@ -1899,12 +1999,20 @@ bool AdvancedOutput::StartRecording() int splitFileTime; int splitFileSize; bool splitFileResetTimestamps; + bool streamOutputsActive = true; + + for (auto &output : streamOutputs) { + if (!obs_output_active(output)) { + streamOutputsActive = false; + break; + } + } if (!useStreamEncoder) { if (!ffmpegOutput) { UpdateRecordingSettings(); } - } else if (!obs_output_active(streamOutput)) { + } else if (!streamOutputsActive) { UpdateStreamSettings(); } @@ -2002,11 +2110,19 @@ bool AdvancedOutput::StartReplayBuffer() const char *rbSuffix; int rbTime; int rbSize; + bool streamOutputsActive = true; + + for (auto &output : streamOutputs) { + if (!obs_output_active(output)) { + streamOutputsActive = false; + break; + } + } if (!useStreamEncoder) { if (!ffmpegOutput) UpdateRecordingSettings(); - } else if (!obs_output_active(streamOutput)) { + } else if (!streamOutputsActive) { UpdateStreamSettings(); } @@ -2072,10 +2188,12 @@ bool AdvancedOutput::StartReplayBuffer() void AdvancedOutput::StopStreaming(bool force) { - if (force) - obs_output_force_stop(streamOutput); - else - obs_output_stop(streamOutput); + for (auto &output : streamOutputs) { + if (force) + obs_output_force_stop(output); + else + obs_output_stop(output); + } } void AdvancedOutput::StopRecording(bool force) @@ -2096,7 +2214,11 @@ void AdvancedOutput::StopReplayBuffer(bool force) bool AdvancedOutput::StreamingActive() const { - return obs_output_active(streamOutput); + for (auto &output : streamOutputs) { + if (obs_output_active(output)) + return true; + } + return false; } bool AdvancedOutput::RecordingActive() const diff --git a/UI/window-basic-main-outputs.hpp b/UI/window-basic-main-outputs.hpp index 88e5d962be34ad..8ae216c87e487b 100644 --- a/UI/window-basic-main-outputs.hpp +++ b/UI/window-basic-main-outputs.hpp @@ -6,7 +6,7 @@ class OBSBasic; struct BasicOutputHandler { OBSOutputAutoRelease fileOutput; - OBSOutputAutoRelease streamOutput; + std::vector streamOutputs; OBSOutputAutoRelease replayBuffer; OBSOutputAutoRelease virtualCam; bool streamingActive = false; @@ -40,8 +40,10 @@ struct BasicOutputHandler { virtual ~BasicOutputHandler(){}; - virtual bool SetupStreaming(obs_service_t *service) = 0; - virtual bool StartStreaming(obs_service_t *service) = 0; + virtual bool + SetupStreaming(const std::vector &services) = 0; + virtual bool + StartStreaming(const std::vector &services) = 0; virtual bool StartRecording() = 0; virtual bool StartReplayBuffer() { return false; } virtual bool StartVirtualCam(); diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 511f389c744834..4849b15b62438b 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1250,9 +1250,8 @@ bool OBSBasic::LoadService() std::vector OBSBasic::ExpandService(obs_service_t *service) { std::vector services; - if (!service) { + if (!service) return services; - } services.push_back(service); @@ -1263,9 +1262,8 @@ std::vector OBSBasic::ExpandService(obs_service_t *service) obs_data_get_array(settings, "backup_servers"); size_t backup_count = obs_data_array_count(backup_servers); - if (backup_count == 0) { + if (backup_count == 0) return services; - } // If service contains one or more backup servers, expand it. const char *name = obs_service_get_name(service); @@ -6532,7 +6530,7 @@ void OBSBasic::StartStreaming() } } - if (!outputHandler->SetupStreaming(services.front())) { + if (!outputHandler->SetupStreaming(services)) { DisplayStreamStartError(); return; } @@ -6552,7 +6550,7 @@ void OBSBasic::StartStreaming() sysTrayStream->setText(ui->streamButton->text()); } - if (!outputHandler->StartStreaming(services.front())) { + if (!outputHandler->StartStreaming(services)) { DisplayStreamStartError(); return; } @@ -6962,9 +6960,8 @@ void OBSBasic::StreamingStart() ui->streamButton->setText(QTStr("Basic.Main.StopStreaming")); ui->streamButton->setEnabled(true); ui->streamButton->setChecked(true); - if (!outputHandler->streamOutput) { - ui->statusbar->StreamStarted(outputHandler->streamOutput); - } + if (!outputHandler->streamOutputs.empty()) + ui->statusbar->StreamStarted(outputHandler->streamOutputs[0]); if (sysTrayStream) { sysTrayStream->setText(ui->streamButton->text()); diff --git a/UI/window-basic-status-bar.cpp b/UI/window-basic-status-bar.cpp index b97ddb8095cda9..f1375dcb39f2e9 100644 --- a/UI/window-basic-status-bar.cpp +++ b/UI/window-basic-status-bar.cpp @@ -461,7 +461,10 @@ void OBSBasicStatusBar::StreamDelayStarting(int sec) if (!main || !main->outputHandler) return; - streamOutput = main->outputHandler->streamOutput; + if (!main->outputHandler->streamOutputs.empty()) + streamOutput = main->outputHandler->streamOutputs[0]; + else + streamOutput = nullptr; delaySecTotal = delaySecStarting = sec; UpdateDelayMsg(); From a70f991d5a767f6158a3f5046bf3127911f541ea Mon Sep 17 00:00:00 2001 From: Obinna Okechukwu Date: Thu, 31 Mar 2022 17:28:16 -0400 Subject: [PATCH 3/4] UI: Add support for redundant streams in bandwidth test This commit adds support for redundant streams (multiple services and output urls) in window-basic-auto-config. It adds a checkbox to the autoconfig page for deciding whether redundant streams should be considered when performing bandwidth tests. It also modifies the TestBandwidthThread() to support streaming to multiple servers simultaneously. Implementation details: In the TestBandwidthThread() implementation, the connected/stopped state which is used to keep track of whether a given output has started/stopped is replaced with a vector of states tracked per output/server url. Instead of checking if a single output is started/stopped as done previously, we now check if all outputs are started/stopped. The start/stop states for a single output along with the output itself and its callbacks are wrapped in an OutputWrapper struct. In bandwidth test calculations, preference is given to the redundant streams servers only if no frames were dropped during the bandwidth test (and the redundant streams checkbox was selected). An additional `preferred` field was added to the ServerInfo struct, which is used to give preference to a given server. Make allowRedundantStreams checkbox visible only when bandwidth test is selected. --- UI/data/locale/en-US.ini | 2 + UI/forms/AutoConfigStreamPage.ui | 30 ++- UI/window-basic-auto-config-test.cpp | 322 ++++++++++++++++++++------- UI/window-basic-auto-config.cpp | 34 ++- UI/window-basic-auto-config.hpp | 25 ++- 5 files changed, 314 insertions(+), 99 deletions(-) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 8109863cc34ecf..0d9904ed8a2326 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -192,6 +192,8 @@ Basic.AutoConfig.StreamPage.StreamKey.LinkToSite="(Link)" Basic.AutoConfig.StreamPage.EncoderKey="Encoder Key" Basic.AutoConfig.StreamPage.ConnectedAccount="Connected account" Basic.AutoConfig.StreamPage.PerformBandwidthTest="Estimate bitrate with bandwidth test (may take a few minutes)" +Basic.AutoConfig.StreamPage.AllowRedundantStreams="Allow redundant streams" +Basic.AutoConfig.StreamPage.AllowRedundantStreamsTooltip="Simultaneously upload a stream and backup copies of the stream to increase reliability." Basic.AutoConfig.StreamPage.PreferHardwareEncoding="Prefer hardware encoding" Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip="Hardware Encoding eliminates most CPU usage, but may require more bitrate to obtain the same level of quality." Basic.AutoConfig.StreamPage.StreamWarning.Title="Stream warning" diff --git a/UI/forms/AutoConfigStreamPage.ui b/UI/forms/AutoConfigStreamPage.ui index b5ee7b1454b4eb..c0748a4fcf378b 100644 --- a/UI/forms/AutoConfigStreamPage.ui +++ b/UI/forms/AutoConfigStreamPage.ui @@ -386,7 +386,23 @@ - + + + + Basic.AutoConfig.StreamPage.AllowRedundantStreamsTooltip + + + Basic.AutoConfig.StreamPage.AllowRedundantStreams + + + false + + + false + + + + BandwidthTest.Region @@ -423,7 +439,7 @@ - + PointingHandCursor @@ -433,7 +449,7 @@ - + Qt::Vertical @@ -446,7 +462,7 @@ - + 7 @@ -475,14 +491,14 @@ - + Basic.AutoConfig.StreamPage.ConnectedAccount - + Qt::Horizontal @@ -495,7 +511,7 @@ - + Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp index 346c92f05d4c5b..7792f126bff4ba 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/UI/window-basic-auto-config-test.cpp @@ -127,9 +127,14 @@ void AutoConfigTestPage::GetServers(std::vector &servers) for (const Json &server : json_services) { const std::string &name = server["name"].string_value(); const std::string &url = server["url"].string_value(); + const vector &urls = server["backup_urls"].array_items(); + + std::vector backup_servers; + for (auto &url : urls) + backup_servers.push_back(url.string_value()); if (wiz->CanTestServer(name.c_str())) { - ServerInfo info(name, url); + ServerInfo info(name, url, backup_servers); servers.push_back(info); } } @@ -150,9 +155,6 @@ const char *FindAudioEncoderFromCodec(const char *type); void AutoConfigTestPage::TestBandwidthThread() { - bool connected = false; - bool stopped = false; - TestMode testMode; testMode.SetVideo(128, 128, 60, 1); @@ -167,6 +169,16 @@ void AutoConfigTestPage::TestBandwidthThread() QMetaObject::invokeMethod(this, "UpdateMessage", Q_ARG(QString, QStringLiteral(""))); + /* -----------------------------------*/ + /* determine which servers to test */ + + std::vector servers; + if (wiz->customServer) + servers.emplace_back(wiz->server.c_str(), wiz->server.c_str(), + wiz->backupServers); + else + GetServers(servers); + /* -----------------------------------*/ /* create obs objects */ @@ -177,8 +189,24 @@ void AutoConfigTestPage::TestBandwidthThread() "obs_x264", "test_x264", nullptr, nullptr); OBSEncoderAutoRelease aencoder = obs_audio_encoder_create( "ffmpeg_aac", "test_aac", nullptr, 0, nullptr); - OBSServiceAutoRelease service = obs_service_create( - serverType, "test_service", nullptr, nullptr); + std::vector services; + + /* -----------------------------------*/ + /* initialize services */ + + size_t max_url_count = 0; + for (auto &server : servers) { + max_url_count = std::max(max_url_count, + 1 + server.backup_servers.size()); + } + + for (size_t i = 0; i < max_url_count; i++) { + // Make a new service per url. + const string name = + QString("test_service#%1").arg(i).toStdString(); + services.emplace_back(obs_service_create( + serverType, name.c_str(), nullptr, nullptr)); + } /* -----------------------------------*/ /* configure settings */ @@ -224,15 +252,6 @@ void AutoConfigTestPage::TestBandwidthThread() config_get_string(main->Config(), "Output", "BindIP"); obs_data_set_string(output_settings, "bind_ip", bind_ip); - /* -----------------------------------*/ - /* determine which servers to test */ - - std::vector servers; - if (wiz->customServer) - servers.emplace_back(wiz->server.c_str(), wiz->server.c_str()); - else - GetServers(servers); - /* just use the first server if it only has one alternate server, * or if using Restream or Nimo TV due to their "auto" servers */ if (servers.size() < 3 || @@ -254,89 +273,133 @@ void AutoConfigTestPage::TestBandwidthThread() /* -----------------------------------*/ /* apply service settings */ - obs_service_update(service, service_settings); - obs_service_apply_encoder_settings(service, vencoder_settings, - aencoder_settings); + for (auto &service : services) { + obs_service_update(service, service_settings); + obs_service_apply_encoder_settings(service, vencoder_settings, + aencoder_settings); + } /* -----------------------------------*/ /* create output */ - const char *output_type = obs_service_get_output_type(service); + const char *output_type = obs_service_get_output_type(services.front()); if (!output_type) output_type = "rtmp_output"; - OBSOutputAutoRelease output = - obs_output_create(output_type, "test_stream", nullptr, nullptr); - obs_output_update(output, output_settings); + std::vector outputs; + bool encoder_updated = false; - const char *audio_codec = obs_output_get_supported_audio_codecs(output); + for (auto &service : services) { + OBSOutputAutoRelease output = obs_output_create( + output_type, "test_stream", nullptr, nullptr); + obs_output_update(output, output_settings); - if (strcmp(audio_codec, "aac") != 0) { - const char *id = FindAudioEncoderFromCodec(audio_codec); - aencoder = obs_audio_encoder_create(id, "test_audio", nullptr, - 0, nullptr); - } + if (!encoder_updated) { + const char *audio_codec = + obs_output_get_supported_audio_codecs(output); - /* -----------------------------------*/ - /* connect encoders/services/outputs */ + if (strcmp(audio_codec, "aac") != 0) { + const char *id = + FindAudioEncoderFromCodec(audio_codec); + aencoder = obs_audio_encoder_create( + id, "test_audio", nullptr, 0, nullptr); + } - obs_encoder_update(vencoder, vencoder_settings); - obs_encoder_update(aencoder, aencoder_settings); - obs_encoder_set_video(vencoder, obs_get_video()); - obs_encoder_set_audio(aencoder, obs_get_audio()); + /* -----------------------------------*/ + /* connect encoders/services/outputs */ - obs_output_set_video_encoder(output, vencoder); - obs_output_set_audio_encoder(output, aencoder, 0); - obs_output_set_reconnect_settings(output, 0, 0); + obs_encoder_update(vencoder, vencoder_settings); + obs_encoder_update(aencoder, aencoder_settings); + obs_encoder_set_video(vencoder, obs_get_video()); + obs_encoder_set_audio(aencoder, obs_get_audio()); + encoder_updated = true; + } + + obs_output_set_video_encoder(output, vencoder); + obs_output_set_audio_encoder(output, aencoder, 0); + obs_output_set_reconnect_settings(output, 0, 0); - obs_output_set_service(output, service); + obs_output_set_service(output, service); + + outputs.emplace_back(OutputWrapper{std::move(output)}); + } /* -----------------------------------*/ /* connect signals */ - auto on_started = [&]() { - unique_lock lock(m); - connected = true; - stopped = false; - cv.notify_one(); - }; + function on_started = + [&](struct OutputWrapper &wrapper) { + unique_lock lock(m); + wrapper.state = OutputWrapper::OutputState::Started; + cv.notify_one(); + }; - auto on_stopped = [&]() { - unique_lock lock(m); - connected = false; - stopped = true; - cv.notify_one(); - }; + function on_stopped = + [&](OutputWrapper &wrapper) { + unique_lock lock(m); + wrapper.state = OutputWrapper::OutputState::Stopped; + cv.notify_one(); + }; - using on_started_t = decltype(on_started); - using on_stopped_t = decltype(on_stopped); + for (OutputWrapper &wrapper : outputs) { + wrapper.start_callback = &on_started; + wrapper.stop_callback = &on_stopped; + } auto pre_on_started = [](void *data, calldata_t *) { - on_started_t &on_started = - *reinterpret_cast(data); - on_started(); + auto &wrapper = *reinterpret_cast(data); + (*wrapper.start_callback)(wrapper); }; auto pre_on_stopped = [](void *data, calldata_t *) { - on_stopped_t &on_stopped = - *reinterpret_cast(data); - on_stopped(); + auto &wrapper = *reinterpret_cast(data); + (*wrapper.stop_callback)(wrapper); }; - signal_handler *sh = obs_output_get_signal_handler(output); - signal_handler_connect(sh, "start", pre_on_started, &on_started); - signal_handler_connect(sh, "stop", pre_on_stopped, &on_stopped); + for (auto &wrapper : outputs) { + signal_handler *sh = + obs_output_get_signal_handler(wrapper.output); + signal_handler_connect(sh, "start", pre_on_started, &wrapper); + signal_handler_connect(sh, "stop", pre_on_stopped, &wrapper); + } + + auto not_started = [&](std::vector &urls) { + for (size_t j = 0; j < urls.size(); j++) { + if (outputs[j].state != + OutputWrapper::OutputState::Started) { + return true; + } + } + return false; + }; + + auto not_stopped = [&](std::vector &urls) { + for (size_t j = 0; j < urls.size(); j++) { + if (outputs[j].state == + OutputWrapper::OutputState::Started) { + return true; + } + } + return false; + }; + + auto force_stop_all = [&]() { + for (auto &wrapper : outputs) { + if (wrapper.state == + OutputWrapper::OutputState::Started) + obs_output_force_stop(wrapper.output); + } + }; /* -----------------------------------*/ /* test servers */ bool success = false; - for (size_t i = 0; i < servers.size(); i++) { auto &server = servers[i]; - connected = false; - stopped = false; + for (auto &wrapper : outputs) + wrapper.state = OutputWrapper::OutputState::Default; int per = int((i + 1) * 100 / servers.size()); QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, per)); @@ -345,28 +408,67 @@ void AutoConfigTestPage::TestBandwidthThread() Q_ARG(QString, QTStr(TEST_BW_CONNECTING) .arg(server.name.c_str()))); - obs_data_set_string(service_settings, "server", - server.address.c_str()); - obs_service_update(service, service_settings); + std::vector urls{server.address}; + urls.insert(urls.end(), server.backup_servers.begin(), + server.backup_servers.end()); - if (!obs_output_start(output)) + if (!wiz->allowRedundantStreams && urls.size() > 1) { continue; + } + + // Setup services for each url. + for (size_t j = 0; j < urls.size(); j++) { + obs_data_set_string(service_settings, "server", + urls[j].c_str()); + obs_service_update(services[j], service_settings); + } + + bool start_failed = false; + + for (size_t j = 0; j < urls.size(); j++) + start_failed |= !obs_output_start(outputs[j].output); unique_lock ul(m); + + if (start_failed) { + ul.unlock(); + force_stop_all(); + continue; + } + if (cancel) { ul.unlock(); - obs_output_force_stop(output); + force_stop_all(); return; } - if (!stopped && !connected) + + do { + // Wait until all outputs are started or stopped. cv.wait(ul); + bool start_attempted = true; + for (size_t j = 0; j < urls.size(); j++) { + if (outputs[j].state == + OutputWrapper::OutputState::Default) { + start_attempted = false; + break; + } + } + if (start_attempted) { + break; + } + } while (true); + if (cancel) { ul.unlock(); - obs_output_force_stop(output); + force_stop_all(); return; } - if (!connected) + + if (not_started(urls)) { + ul.unlock(); + force_stop_all(); continue; + } QMetaObject::invokeMethod( this, "UpdateMessage", @@ -376,47 +478,89 @@ void AutoConfigTestPage::TestBandwidthThread() /* ignore first 2.5 seconds due to possible buffering skewing * the result */ cv.wait_for(ul, chrono::milliseconds(2500)); - if (stopped) + + if (not_started(urls)) { + ul.unlock(); + force_stop_all(); continue; + } + if (cancel) { ul.unlock(); - obs_output_force_stop(output); + force_stop_all(); return; } /* continue test */ - int start_bytes = (int)obs_output_get_total_bytes(output); + std::vector start_bytes(urls.size()); + for (size_t j = 0; j < urls.size(); j++) { + start_bytes[j] = (int)obs_output_get_total_bytes( + outputs[j].output); + } + uint64_t t_start = os_gettime_ns(); cv.wait_for(ul, chrono::seconds(10)); - if (stopped) + + if (not_started(urls)) { + ul.unlock(); + force_stop_all(); continue; + } + if (cancel) { ul.unlock(); - obs_output_force_stop(output); + force_stop_all(); return; } - obs_output_stop(output); - cv.wait(ul); + // Stop all outputs and wait for all to stop. + for (size_t j = 0; j < urls.size(); j++) { + if (outputs[j].state == + OutputWrapper::OutputState::Started) { + obs_output_stop(outputs[j].output); + } + } + + while (not_stopped(urls)) + cv.wait(ul); uint64_t total_time = os_gettime_ns() - t_start; if (total_time == 0) total_time = 1; - int total_bytes = - (int)obs_output_get_total_bytes(output) - start_bytes; + uint64_t total_bytes = 0; + int frames_dropped = 0; + + for (size_t j = 0; j < urls.size(); j++) { + total_bytes += (int)obs_output_get_total_bytes( + outputs[j].output) - + start_bytes[j]; + frames_dropped += obs_output_get_frames_dropped( + outputs[j].output); + } + uint64_t bitrate = util_mul_div64( total_bytes, 8ULL * 1000000000ULL / 1000ULL, total_time); - if (obs_output_get_frames_dropped(output) || - (int)bitrate < (wiz->startingBitrate * 75 / 100)) { - server.bitrate = (int)bitrate * 70 / 100; + const int avg_bitrate = (int)bitrate / urls.size(); + + if (frames_dropped || + (int)avg_bitrate < (wiz->startingBitrate * 75 / 100)) { + server.bitrate = (int)avg_bitrate * 70 / 100; + server.preferred = false; } else { server.bitrate = wiz->startingBitrate; + server.preferred = wiz->allowRedundantStreams && + urls.size() > 1; + } + + for (size_t j = 0; j < urls.size(); j++) { + server.ms = std::max(server.ms, + obs_output_get_connect_time_ms( + outputs[j].output)); } - server.ms = obs_output_get_connect_time_ms(output); success = true; } @@ -431,20 +575,26 @@ void AutoConfigTestPage::TestBandwidthThread() int bestMS = 0x7FFFFFFF; string bestServer; string bestServerName; + std::vector bestBackupServers; + bool isBestPreferred = false; for (auto &server : servers) { bool close = abs(server.bitrate - bestBitrate) < 400; if ((!close && server.bitrate > bestBitrate) || - (close && server.ms < bestMS)) { + (close && !isBestPreferred && server.ms < bestMS) || + (close && !isBestPreferred && server.preferred)) { bestServer = server.address; bestServerName = server.name; + bestBackupServers = server.backup_servers; bestBitrate = server.bitrate; bestMS = server.ms; + isBestPreferred = server.preferred; } } wiz->server = std::move(bestServer); + wiz->backupServers = std::move(bestBackupServers); wiz->serverName = std::move(bestServerName); wiz->idealBitrate = bestBitrate; diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index b38c1d743dc529..081f211b7cf2cf 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include "window-basic-auto-config.hpp" #include "window-basic-main.hpp" @@ -57,13 +59,16 @@ static OBSData OpenServiceSettings(std::string &type) } static void GetServiceInfo(std::string &type, std::string &service, - std::string &server, std::string &key) + std::string &server, + std::vector &backupServers, + std::string &key) { OBSData settings = OpenServiceSettings(type); service = obs_data_get_string(settings, "service"); server = obs_data_get_string(settings, "server"); key = obs_data_get_string(settings, "key"); + backupServers = get_backup_servers(settings); } /* ------------------------------------------------------------------------- */ @@ -366,12 +371,23 @@ bool AutoConfigStreamPage::validatePage() if (wiz->customServer) { QString server = ui->customServer->text().trimmed(); wiz->server = wiz->serverName = QT_TO_UTF8(server); + wiz->backupServers.clear(); } else { wiz->serverName = QT_TO_UTF8(ui->server->currentText()); wiz->server = QT_TO_UTF8(ui->server->currentData().toString()); + wiz->backupServers.clear(); + + QJsonObject data = ui->server->currentData().toJsonObject(); + QJsonArray servers = data.value("backup_servers").toArray(); + + for (auto srv : servers) { + wiz->backupServers.push_back( + srv.toString().toStdString()); + } } wiz->bandwidthTest = ui->doBandwidthTest->isChecked(); + wiz->allowRedundantStreams = ui->allowRedundantStreams->isChecked(); wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate"); wiz->idealBitrate = wiz->startingBitrate; wiz->regionUS = ui->regionUS->isChecked(); @@ -641,6 +657,10 @@ void AutoConfigStreamPage::ServiceChanged() ui->serverLabel->setHidden(testBandwidth); } + const bool isYouTube = (service.rfind("YouTube", 0) == 0); + ui->allowRedundantStreams->setVisible(isYouTube && + ui->doBandwidthTest->isChecked()); + wiz->testRegions = regionBased && testBandwidth; ui->bitrateLabel->setHidden(testBandwidth); @@ -707,7 +727,7 @@ AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) installEventFilter(CreateShortcutFilter()); std::string serviceType; - GetServiceInfo(serviceType, serviceName, server, key); + GetServiceInfo(serviceType, serviceName, server, backupServers, key); #if defined(_WIN32) || defined(__APPLE__) setWizardStyle(QWizard::ModernStyle); #endif @@ -772,7 +792,7 @@ AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) if (!customServer) { QComboBox *serverList = streamPage->ui->server; - int idx = serverList->findData(QString(server.c_str())); + int idx = find_server_index(server, backupServers, serverList); if (idx == -1) idx = 0; @@ -900,6 +920,14 @@ void AutoConfig::SaveStreamSettings() if (!customServer) obs_data_set_string(settings, "service", serviceName.c_str()); obs_data_set_string(settings, "server", server.c_str()); + + QJsonArray qBackupServers; + for (size_t i = 0; i < backupServers.size(); i++) + qBackupServers.append(QString::fromStdString(backupServers[i])); + + obs_data_set_array(settings, "backup_servers", + backup_servers_to_data_array(qBackupServers)); + #if YOUTUBE_ENABLED if (!IsYouTubeService(serviceName)) obs_data_set_string(settings, "key", key.c_str()); diff --git a/UI/window-basic-auto-config.hpp b/UI/window-basic-auto-config.hpp index 6f3a6aee4d0c0e..f05390570e5419 100644 --- a/UI/window-basic-auto-config.hpp +++ b/UI/window-basic-auto-config.hpp @@ -87,6 +87,7 @@ class AutoConfig : public QWizard { std::string serviceName; std::string serverName; std::string server; + std::vector backupServers; std::string key; bool hardwareEncodingAvailable = false; @@ -97,6 +98,7 @@ class AutoConfig : public QWizard { int startingBitrate = 2500; bool customServer = false; bool bandwidthTest = false; + bool allowRedundantStreams = false; bool testRegions = true; bool twitchAuto = false; bool regionUS = true; @@ -242,18 +244,35 @@ class AutoConfigTestPage : public QWizardPage { struct ServerInfo { std::string name; std::string address; + std::vector backup_servers; int bitrate = 0; int ms = -1; + bool preferred = false; // prefer server in bandwidth test. inline ServerInfo() {} - inline ServerInfo(const std::string &name_, - const std::string &address_) - : name(name_), address(address_) + inline ServerInfo( + const std::string &name_, const std::string &address_, + const std::vector &backup_servers_) + : name(name_), + address(address_), + backup_servers(backup_servers_) { } }; + struct OutputWrapper { + OBSOutputAutoRelease output; + enum class OutputState { + Default, + Started, + Stopped, + }; + OutputState state = OutputState::Default; + std::function *start_callback; + std::function *stop_callback; + }; + void GetServers(std::vector &servers); public: From a5de4f2c408cd47715a0b03b3d4bc6fbe2ee4475 Mon Sep 17 00:00:00 2001 From: Obinna Okechukwu Date: Fri, 22 Apr 2022 21:28:33 -0400 Subject: [PATCH 4/4] UI: Show backup/redundant streams in stats page --- UI/window-basic-main.cpp | 9 ++++++ UI/window-basic-main.hpp | 1 + UI/window-basic-settings.cpp | 5 +++ UI/window-basic-stats.cpp | 59 +++++++++++++++++++++++++++--------- UI/window-basic-stats.hpp | 4 ++- 5 files changed, 62 insertions(+), 16 deletions(-) diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 4849b15b62438b..2fde259b4e3d27 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -4280,6 +4280,15 @@ std::vector OBSBasic::GetServices() return services; } +std::vector OBSBasic::GetStreamingOutputs() +{ + std::vector outputs; + for (OBSOutputAutoRelease &output : outputHandler->streamOutputs) + outputs.push_back(output.Get()); + + return std::move(outputs); +} + void OBSBasic::SetService(obs_service_t *newService) { if (newService) { diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 116f97e0af17cf..caf600a15cf9f4 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -830,6 +830,7 @@ private slots: } std::vector GetServices(); + std::vector GetStreamingOutputs(); void SetService(obs_service_t *service); diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 3289f193744039..17c6e8241a30b0 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -916,6 +916,11 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) ui->buttonBox->button(QDialogButtonBox::Ok)->setIcon(QIcon()); ui->buttonBox->button(QDialogButtonBox::Cancel)->setIcon(QIcon()); + connect(ui->buttonBox->button(QDialogButtonBox::Apply), + SIGNAL(clicked()), main, SLOT(ResetStatsHotkey())); + connect(ui->buttonBox->button(QDialogButtonBox::Ok), SIGNAL(clicked()), + main, SLOT(ResetStatsHotkey())); + SimpleRecordingQualityChanged(); AdvOutSplitFileChanged(); diff --git a/UI/window-basic-stats.cpp b/UI/window-basic-stats.cpp index 4719567dac3945..e1e64414b77096 100644 --- a/UI/window-basic-stats.cpp +++ b/UI/window-basic-stats.cpp @@ -1,7 +1,6 @@ #include "obs-frontend-api/obs-frontend-api.h" #include "window-basic-stats.hpp" -#include "window-basic-main.hpp" #include "platform.hpp" #include "obs-app.hpp" #include "qt-wrappers.hpp" @@ -133,10 +132,8 @@ OBSBasicStats::OBSBasicStats(QWidget *parent, bool closeable) addOutputCol("Basic.Stats.Bitrate"); /* --------------------------------------------- */ - - AddOutputLabels(QTStr("Basic.Stats.Output.Stream")); - AddOutputLabels(QTStr("Basic.Stats.Output.Recording")); - + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + CreateOutputLabels(main); /* --------------------------------------------- */ QVBoxLayout *outputContainerLayout = new QVBoxLayout(); @@ -193,8 +190,6 @@ OBSBasicStats::OBSBasicStats(QWidget *parent, bool closeable) &OBSBasicStats::RecordingTimeLeft); recTimeLeft.setInterval(REC_TIME_LEFT_INTERVAL); - OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); - const char *geometry = config_get_string(main->Config(), "Stats", "geometry"); if (geometry != NULL) { @@ -218,6 +213,26 @@ OBSBasicStats::OBSBasicStats(QWidget *parent, bool closeable) StartRecTimeLeft(); } +void OBSBasicStats::deleteLabel(OutputLabels label) +{ + label.name->deleteLater(); + label.status->deleteLater(); + label.droppedFrames->deleteLater(); + label.megabytesSent->deleteLater(); + label.bitrate->deleteLater(); +} + +void OBSBasicStats::CreateOutputLabels(OBSBasic *main) +{ + foreach(OutputLabels label, outputLabels) deleteLabel(label); + outputLabels.clear(); + int servicesCount = main->GetServices().size(); + for (int i = 0; i < servicesCount; i++) + AddOutputLabels(QTStr("Basic.Stats.Output.Stream")); + + AddOutputLabels(QTStr("Basic.Stats.Output.Recording")); +} + void OBSBasicStats::closeEvent(QCloseEvent *event) { OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); @@ -287,10 +302,10 @@ void OBSBasicStats::Update() struct obs_video_info ovi = {}; obs_get_video_info(&ovi); - OBSOutputAutoRelease strOutput = obs_frontend_get_streaming_output(); + auto streamingOutputs = main->GetStreamingOutputs(); OBSOutputAutoRelease recOutput = obs_frontend_get_recording_output(); - if (!strOutput && !recOutput) + if (streamingOutputs.empty() && !recOutput) return; /* ------------------------------------------- */ @@ -429,12 +444,16 @@ void OBSBasicStats::Update() /* ------------------------------------------- */ /* recording/streaming stats */ + int recordingIndex = (int)outputLabels.size() - 1; + int numOutputs = (int)streamingOutputs.size(); - outputLabels[0].Update(strOutput, false); - outputLabels[1].Update(recOutput, true); + for (int i = 0; i < recordingIndex && i < numOutputs; i++) + outputLabels[i].Update(streamingOutputs[i], false); + + outputLabels[recordingIndex].Update(recOutput, true); if (obs_output_active(recOutput)) { - long double kbps = outputLabels[1].kbps; + long double kbps = outputLabels[recordingIndex].kbps; bitrates.push_back(kbps); } } @@ -485,11 +504,21 @@ void OBSBasicStats::Reset() first_rendered = 0xFFFFFFFF; first_lagged = 0xFFFFFFFF; - OBSOutputAutoRelease strOutput = obs_frontend_get_streaming_output(); + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + CreateOutputLabels(main); + + auto streamingOutputs = main->GetStreamingOutputs(); + OBSOutputAutoRelease recOutput = obs_frontend_get_recording_output(); - outputLabels[0].Reset(strOutput); - outputLabels[1].Reset(recOutput); + int recordingIndex = (int)outputLabels.size() - 1; + int numOutputs = (int)streamingOutputs.size(); + + for (int i = 0; i < recordingIndex && i < numOutputs; i++) + outputLabels[i].Reset(streamingOutputs[i]); + + outputLabels[recordingIndex].Reset(recOutput); + Update(); } diff --git a/UI/window-basic-stats.hpp b/UI/window-basic-stats.hpp index d760f64911c5f0..3f53d7c90ea769 100644 --- a/UI/window-basic-stats.hpp +++ b/UI/window-basic-stats.hpp @@ -8,6 +8,7 @@ #include #include #include +#include "window-basic-main.hpp" class QGridLayout; class QCloseEvent; @@ -57,9 +58,10 @@ class OBSBasicStats : public QWidget { void AddOutputLabels(QString name); void Update(); + void CreateOutputLabels(OBSBasic *main); virtual void closeEvent(QCloseEvent *event) override; - + void deleteLabel(OutputLabels label); static void OBSFrontendEvent(enum obs_frontend_event event, void *ptr); public: