diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp index dc3fa633719ca2..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); } @@ -506,7 +509,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/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/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-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 6976083319985c..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; @@ -892,7 +912,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(); @@ -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: 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-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..2fde259b4e3d27 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,52 @@ 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); - return !!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"); + + 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 +1295,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 +2661,7 @@ OBSBasic::~OBSBasic() obs_hotkey_set_callback_routing_func(nullptr, nullptr); ClearHotkeys(); - service = nullptr; + services.clear(); outputHandler.reset(); if (interaction) @@ -4225,20 +4269,30 @@ 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; +} + +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) { - service = newService; + services = ExpandService(newService); } } @@ -6348,16 +6402,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 +6539,7 @@ void OBSBasic::StartStreaming() } } - if (!outputHandler->SetupStreaming(service)) { + if (!outputHandler->SetupStreaming(services)) { DisplayStreamStartError(); return; } @@ -6500,7 +6559,7 @@ void OBSBasic::StartStreaming() sysTrayStream->setText(ui->streamButton->text()); } - if (!outputHandler->StartStreaming(service)) { + if (!outputHandler->StartStreaming(services)) { DisplayStreamStartError(); return; } @@ -6910,7 +6969,8 @@ 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->streamOutputs.empty()) + ui->statusbar->StreamStarted(outputHandler->streamOutputs[0]); if (sysTrayStream) { sysTrayStream->setText(ui->streamButton->text()); @@ -6920,7 +6980,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 +7605,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..caf600a15cf9f4 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,9 @@ private slots: return OBSSource(obs_scene_get_source(curScene)); } - obs_service_t *GetService(); + std::vector GetServices(); + std::vector GetStreamingOutputs(); + void SetService(obs_service_t *service); int GetTransitionDuration(); @@ -874,6 +876,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() 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: 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();