diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index d060dd96b..035644ca0 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -84,6 +84,24 @@ target_sources( ui-validation.cpp ui-validation.hpp) +target_sources( + obs-studio + PRIVATE # cmake-format: sortable + goliveapi-censoredjson.cpp + goliveapi-censoredjson.hpp + goliveapi-network.cpp + goliveapi-network.hpp + goliveapi-postdata.cpp + goliveapi-postdata.hpp + models/multitrack-video.hpp + multitrack-video-error.cpp + multitrack-video-error.hpp + multitrack-video-output.cpp + multitrack-video-output.hpp + qt-helpers.cpp + qt-helpers.hpp + system-info.hpp) + if(OS_WINDOWS) include(cmake/os-windows.cmake) elseif(OS_MACOS) diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp index 39f260277..12f036049 100644 --- a/UI/api-interface.cpp +++ b/UI/api-interface.cpp @@ -482,6 +482,16 @@ struct OBSStudioAPI : obs_frontend_callbacks { obs_output_t *obs_frontend_get_streaming_output(void) override { + auto multitrackVideo = + main->outputHandler->multitrackVideo.get(); + auto mtvOutput = + multitrackVideo + ? obs_output_get_ref( + multitrackVideo->StreamingOutput()) + : nullptr; + if (mtvOutput) + return mtvOutput; + OBSOutput output = main->outputHandler->streamOutput.Get(); return obs_output_get_ref(output); } diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index 586b1149f..5351f16f6 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -280,6 +280,23 @@ target_sources( window-remux.cpp window-remux.hpp) +target_sources( + obs + PRIVATE # cmake-format: sortable + goliveapi-censoredjson.cpp + goliveapi-censoredjson.hpp + goliveapi-network.cpp + goliveapi-network.hpp + goliveapi-postdata.cpp + goliveapi-postdata.hpp + multitrack-video-error.cpp + multitrack-video-error.hpp + multitrack-video-output.cpp + multitrack-video-output.hpp + qt-helpers.cpp + qt-helpers.hpp + system-info.hpp) + target_sources(obs PRIVATE importers/importers.cpp importers/importers.hpp importers/classic.cpp importers/sl.cpp importers/studio.cpp importers/xsplit.cpp) @@ -366,6 +383,8 @@ if(OS_WINDOWS) win-update/updater/manifest.hpp ${CMAKE_BINARY_DIR}/obs.rc) + target_sources(obs PRIVATE system-info-windows.cpp) + find_package(MbedTLS) target_link_libraries(obs PRIVATE Mbedtls::Mbedtls nlohmann_json::nlohmann_json OBS::blake2 Detours::Detours) @@ -426,6 +445,8 @@ elseif(OS_MACOS) target_sources(obs PRIVATE platform-osx.mm) target_sources(obs PRIVATE forms/OBSPermissions.ui window-permissions.cpp window-permissions.hpp) + target_sources(obs PRIVATE system-info-macos.mm) + if(ENABLE_WHATSNEW) find_library(SECURITY Security) find_package(nlohmann_json REQUIRED) @@ -462,6 +483,8 @@ elseif(OS_POSIX) target_sources(obs PRIVATE platform-x11.cpp) target_link_libraries(obs PRIVATE Qt::GuiPrivate Qt::DBus) + target_sources(obs PRIVATE system-info-posix.cpp) + target_compile_definitions(obs PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}" "$<$:LINUX_PORTABLE>") if(TARGET obspython) diff --git a/UI/cmake/os-freebsd.cmake b/UI/cmake/os-freebsd.cmake index 6ad52b1c4..323600567 100644 --- a/UI/cmake/os-freebsd.cmake +++ b/UI/cmake/os-freebsd.cmake @@ -2,6 +2,8 @@ target_sources(obs-studio PRIVATE platform-x11.cpp) target_compile_definitions(obs-studio PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}") target_link_libraries(obs-studio PRIVATE Qt::GuiPrivate procstat) +target_sources(obs-studio PRIVATE system-info-posix.cpp) + if(TARGET OBS::python) find_package(Python REQUIRED COMPONENTS Interpreter Development) target_link_libraries(obs-studio PRIVATE Python::Python) diff --git a/UI/cmake/os-linux.cmake b/UI/cmake/os-linux.cmake index 37a02268f..0ffb441e4 100644 --- a/UI/cmake/os-linux.cmake +++ b/UI/cmake/os-linux.cmake @@ -2,6 +2,8 @@ target_sources(obs-studio PRIVATE platform-x11.cpp) target_compile_definitions(obs-studio PRIVATE OBS_INSTALL_PREFIX="${OBS_INSTALL_PREFIX}") target_link_libraries(obs-studio PRIVATE Qt::GuiPrivate Qt::DBus) +target_sources(obs-studio PRIVATE system-info-posix.cpp) + if(TARGET OBS::python) find_package(Python REQUIRED COMPONENTS Interpreter Development) target_link_libraries(obs-studio PRIVATE Python::Python) diff --git a/UI/cmake/os-macos.cmake b/UI/cmake/os-macos.cmake index d9354ec4d..5003e0b62 100644 --- a/UI/cmake/os-macos.cmake +++ b/UI/cmake/os-macos.cmake @@ -3,6 +3,8 @@ include(cmake/feature-sparkle.cmake) target_sources(obs-studio PRIVATE platform-osx.mm forms/OBSPermissions.ui window-permissions.cpp window-permissions.hpp) target_compile_options(obs-studio PRIVATE -Wno-quoted-include-in-framework-header -Wno-comma) +target_sources(obs-studio PRIVATE system-info-macos.mm) + set_source_files_properties(platform-osx.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 14.0.3) diff --git a/UI/cmake/os-windows.cmake b/UI/cmake/os-windows.cmake index 35180a37e..83ced77f0 100644 --- a/UI/cmake/os-windows.cmake +++ b/UI/cmake/os-windows.cmake @@ -33,6 +33,8 @@ target_sources( win-dll-blocklist.c win-update/updater/manifest.hpp) +target_sources(obs-studio PRIVATE system-info-windows.cpp) + target_link_libraries(obs-studio PRIVATE crypt32 OBS::blake2 OBS::w32-pthreads MbedTLS::MbedTLS nlohmann_json::nlohmann_json Detours::Detours) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 5a9cc0d62..28a5e2854 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -115,6 +115,7 @@ MixerToolbarMenu="Audio Mixer Menu" SceneFilters="Open Scene Filters" List="List" Grid="Grid" +Automatic="Automatic" # warning for plugin load failures PluginsFailedToLoad.Title="Plugin Load Error" @@ -224,6 +225,7 @@ 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" Basic.AutoConfig.StreamPage.StreamWarning.Text="The bandwidth test is about to stream randomized video data without audio to your channel. If you're able, it's recommended to temporarily turn off saving videos of streams and set the stream to private until after the test has completed. Continue?" +Basic.AutoConfig.StreamPage.UseMultitrackVideo="Test %1" Basic.AutoConfig.TestPage="Final Results" Basic.AutoConfig.TestPage.SubTitle.Testing="The program is now executing a set of tests to estimate the ideal settings" Basic.AutoConfig.TestPage.SubTitle.Complete="Testing complete" @@ -242,6 +244,7 @@ Basic.AutoConfig.TestPage.Result.Header="The program has determined that these e Basic.AutoConfig.TestPage.Result.Footer="To use these settings, click Apply Settings. To reconfigure the wizard and try again, click Back. To manually configure settings yourself, click Cancel and open Settings." Basic.AutoConfig.Info="The auto-configuration wizard will determine the best settings based on your computer specs and internet speed." Basic.AutoConfig.RunAnytime="This can be run at any time by going to the Tools menu." +Basic.AutoConfig.TestPage.Result.StreamingResolution="Streaming (Scaled) Resolution" # stats Basic.Stats="Stats" @@ -721,6 +724,7 @@ Basic.Main.Scenes="Scenes" Basic.Main.Sources="Sources" Basic.Main.Source="Source" Basic.Main.Controls="Controls" +Basic.Main.PreparingStream="Preparing..." Basic.Main.Connecting="Connecting..." Basic.Main.StartRecording="Start Recording" Basic.Main.StartReplayBuffer="Start Replay Buffer" @@ -860,6 +864,7 @@ Basic.MainMenu.Help.About="&About" Basic.Settings.ProgramRestart="The program must be restarted for these settings to take effect." Basic.Settings.ConfirmTitle="Confirm Changes" Basic.Settings.Confirm="You have unsaved changes. Save changes?" +Basic.Settings.MultitrackVideoDisabledSettings="%1 %2 is controlling some of your stream settings" # basic mode 'general' settings Basic.Settings.General="General" @@ -934,6 +939,7 @@ Basic.Settings.Appearance.General.NoVariant="No Styles Available" # basic mode 'stream' settings Basic.Settings.Stream="Stream" +Basic.Settings.Stream.Destination="Destination" Basic.Settings.Stream.Custom.UseAuthentication="Use authentication" Basic.Settings.Stream.Custom.Username="Username" Basic.Settings.Stream.Custom.Password="Password" @@ -957,6 +963,18 @@ Basic.Settings.Stream.Recommended.MaxVideoBitrate="Maximum Video Bitrate: %1 kbp Basic.Settings.Stream.Recommended.MaxAudioBitrate="Maximum Audio Bitrate: %1 kbps" Basic.Settings.Stream.Recommended.MaxResolution="Maximum Resolution: %1" Basic.Settings.Stream.Recommended.MaxFPS="Maximum FPS: %1" +Basic.Settings.Stream.SpecifyCustomServer="Specify Custom Server..." +Basic.Settings.Stream.ServiceCustomServer="Custom Server" +Basic.Settings.Stream.EnableMultitrackVideo="Enable %1" +Basic.Settings.Stream.MultitrackVideoMaximumAggregateBitrate="Maximum Streaming Bandwidth" +Basic.Settings.Stream.MultitrackVideoMaximumAggregateBitrateAuto="Auto" +Basic.Settings.Stream.MultitrackVideoMaximumVideoTracks="Maximum Video Tracks" +Basic.Settings.Stream.MultitrackVideoMaximumVideoTracksAuto="Auto" +Basic.Settings.Stream.MultitrackVideoStreamDumpEnable="Enable stream dump to FLV (uses simple recording file settings)" +Basic.Settings.Stream.MultitrackVideoConfigOverride="Config Override (JSON)" +Basic.Settings.Stream.MultitrackVideoConfigOverrideEnable="Enable Config Override" +Basic.Settings.Stream.MultitrackVideoLabel="Multitrack Video" +Basic.Settings.Stream.AdvancedOptions="Advanced Options" # basic mode 'output' settings Basic.Settings.Output="Output" @@ -1541,3 +1559,29 @@ YouTube.Errors.rateLimitExceeded="You are sending messages too quickly." # Browser Dock YouTube.DocksRemoval.Title="Clear Legacy YouTube Browser Docks" YouTube.DocksRemoval.Text="These browser docks will be removed as deprecated:\n\n%1\nUse \"Docks/YouTube Live Control Room\" instead." + +# MultitrackVideo +ConfigDownload.WarningMessageTitle="Warning" +FailedToStartStream.MissingConfigURL="No config URL available for the current service" +FailedToStartStream.NoCustomRTMPURLInSettings="Custom RTMP URL not specified" +FailedToStartStream.InvalidCustomConfig="Invalid custom config" +FailedToStartStream.FailedToCreateMultitrackVideoService="Failed to create multitrack video service" +FailedToStartStream.FailedToCreateMultitrackVideoOutput="Failed to create multitrack video rtmp output" +FailedToStartStream.EncoderNotAvailable="NVENC not available.\n\nFailed to find encoder type '%1'" +FailedToStartStream.FailedToCreateVideoEncoder="Failed to create video encoder '%1' (type: '%2')" +FailedToStartStream.FailedToGetOBSVideoInfo="Failed to get obs video info while creating encoder '%1' (type: '%2')" +FailedToStartStream.FailedToCreateAudioEncoder="Failed to create audio encoder" +FailedToStartStream.NoRTMPURLInConfig="Config does not contain stream target RTMP(S) URL" +FailedToStartStream.FallbackToDefault="Starting the stream using %1 failed; do you want to retry using single encode settings?" +FailedToStartStream.ConfigRequestFailed="Could not fetch config from %1

HTTP error: %2" +FailedToStartStream.WarningUnknownStatus="Received unknown status value '%1'" +FailedToStartStream.WarningRetryNonMultitrackVideo="\n

\nDo you want to continue streaming without %1?" +FailedToStartStream.WarningRetry="\n

\nDo you want to continue streaming?" +FailedToStartStream.MissingEncoderConfigs="Go live config did not include encoder configurations" +FailedToStartStream.StatusMissingHTML="Go live request returned an unspecified error" +FailedToStartStream.NoConfigSupplied="Missing config" +MultitrackVideo.Info="%1 automatically optimizes your settings to encode and send multiple video qualities. Selecting this option will send %2 information about your computer and software setup." +MultitrackVideo.IncompatibleSettings.Title="Incompatible Settings" +MultitrackVideo.IncompatibleSettings.Text="%1 is not currently compatible with:\n\n%2\nTo continue streaming with %1, disable incompatible settings:\n\n%3\nand Start Streaming again." +MultitrackVideo.IncompatibleSettings.DisableAndStartStreaming="Disable for this stream and Start Streaming" +MultitrackVideo.IncompatibleSettings.UpdateAndStartStreaming="Update Settings and Start Streaming" diff --git a/UI/data/themes/Yami.obt b/UI/data/themes/Yami.obt index cf9230b77..91b48ddc4 100644 --- a/UI/data/themes/Yami.obt +++ b/UI/data/themes/Yami.obt @@ -1376,6 +1376,11 @@ QLabel#errorLabel { font-weight: bold; } +QFrame [themeID="notice"] { + background: var(--bg_preview); + border-radius: var(--border_radius); +} + /* About dialog */ * [themeID="aboutName"] { diff --git a/UI/forms/AutoConfigStreamPage.ui b/UI/forms/AutoConfigStreamPage.ui index b52f4d02a..241292c6d 100644 --- a/UI/forms/AutoConfigStreamPage.ui +++ b/UI/forms/AutoConfigStreamPage.ui @@ -330,7 +330,73 @@ - + + + + PointingHandCursor + + + Basic.AutoConfig.StreamPage.ConnectAccount + + + + + + + Basic.AutoConfig.StreamPage.ConnectedAccount + + + + + + + 7 + + + 7 + + + + + + true + + + + Auth.LoadingChannel.Title + + + + + + + Basic.AutoConfig.StreamPage.DisconnectAccount + + + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced + + + + Basic.Settings.Output.VideoBitrate @@ -340,7 +406,7 @@ - + @@ -356,7 +422,7 @@ - + Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip @@ -369,7 +435,7 @@ - + Basic.AutoConfig.StreamPage.PerformBandwidthTest @@ -379,7 +445,49 @@ - + + + + Basic.AutoConfig.StreamPage.UseMultitrackVideo + + + true + + + + + + + MultitrackVideo.Info + + + Qt::RichText + + + true + + + true + + + QSizePolicy::MinimumExpanding, QSizePolicy::Minimum + + + + + + + Qt::Vertical + + + + 6 + 6 + + + + + BandwidthTest.Region @@ -416,85 +524,6 @@ - - - - PointingHandCursor - - - Basic.AutoConfig.StreamPage.ConnectAccount - - - - - - - Qt::Vertical - - - - 6 - 6 - - - - - - - - 7 - - - 7 - - - - - - true - - - - Auth.LoadingChannel.Title - - - - - - - Basic.AutoConfig.StreamPage.DisconnectAccount - - - - - - - - - Basic.AutoConfig.StreamPage.ConnectedAccount - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced - - - diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index cf0e4fbd5..45af7e598 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -1204,353 +1204,614 @@ - - - - QFormLayout::AllNonFixedFieldsGrow + + + QFrame::NoFrame + + + QFrame::Plain + + + true + + + + + 0 + 0 + - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - Basic.AutoConfig.StreamPage.Server - - - - - - - - 0 - 0 - - - - 1 - - - + + + + + + 0 + 0 + + + + Basic.Settings.Stream.Destination + + + + QFormLayout::AllNonFixedFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + - 0 + 9 - 0 + 2 - 0 + 9 - 0 + 9 - - + + + + Basic.AutoConfig.StreamPage.Server + + + + + + + + 0 + 0 + + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + Basic.Settings.Stream.ServiceCustomServer + + + serviceCustomServer + + + + + + + + + + Basic.AutoConfig.StreamPage.StreamKey + + + true + + + key + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + QLineEdit::Password + + + + + + + Show + + + + + + + + + + -4 + + + Basic.AutoConfig.StreamPage.GetStreamKey + + + + + + + + + + Qt::Horizontal + + + + 170 + 0 + + + + + + + + + + PointingHandCursor + + + Basic.AutoConfig.StreamPage.ConnectAccount + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + + + Basic.AutoConfig.StreamPage.ConnectedAccount + + + + + + + 8 + + + 7 + + + 7 + + + + + font-weight: bold + + + Auth.LoadingChannel.Title + + + + + + + Basic.AutoConfig.StreamPage.DisconnectAccount + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + + + Basic.Settings.Stream.Custom.UseAuthentication + + + + + + + Basic.Settings.Stream.Custom.Username + + + authUsername + + + + + + + + + + Basic.Settings.Stream.Custom.Password + + + authPw + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QLineEdit::Password + + + + + + + Show + + + + + - - + + + + + + 0 + 0 + + + + Basic.Settings.Stream.MultitrackVideoLabel + + + + QFormLayout::AllNonFixedFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + - 0 + 9 - 0 + 2 - 0 + 9 - 0 + 9 - - + + + + Basic.Settings.Stream.EnableMultitrackVideo + + + + + + + MultitrackVideo.Info + + + Qt::RichText + + + true + + + true + + + + + + + Basic.Settings.Stream.MultitrackVideoMaximumAggregateBitrate + + + + + + + + + Basic.Settings.Stream.MultitrackVideoMaximumAggregateBitrateAuto + + + QSizePolicy::Minimum, QSizePolicy::Minimum + + + + + + + QSizePolicy::MinimumExpanding, QSizePolicy::Minimum + + + 500 + + + 1000000 + + + 8000 + + + + + + + + + Basic.Settings.Stream.MultitrackVideoMaximumVideoTracks + + + + + + + + + Basic.Settings.Stream.MultitrackVideoMaximumVideoTracksAuto + + + QSizePolicy::Minimum, QSizePolicy::Minimum + + + + + + + QSizePolicy::MinimumExpanding, QSizePolicy::Minimum + + + 0 + + + 100 + + + 0 + + + + + + + + + Basic.Settings.Stream.MultitrackVideoStreamDumpEnable + + + + + + + Basic.Settings.Stream.MultitrackVideoConfigOverrideEnable + + + + + + + Basic.Settings.Stream.MultitrackVideoConfigOverride + + + + + + + QSizePolicy::Preferred, QSizePolicy::MinimumExpanding + + - - - - - - Basic.AutoConfig.StreamPage.StreamKey - - - true - - - key - - - - - - - - 0 + + + + + + 0 + 0 + - - 0 + + Basic.Settings.Stream.AdvancedOptions - - 0 - - - 0 - - - - - - - - - - - QLineEdit::Password - - - - - - - Show - - - - - - - - - - -4 - - - Basic.AutoConfig.StreamPage.GetStreamKey - - - - - - - - - - Qt::Horizontal - - - - 170 - 8 - - - - - - - - 8 - - - 7 - - - 7 - - - - - font-weight: bold + + + QFormLayout::AllNonFixedFieldsGrow - - Auth.LoadingChannel.Title + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - Basic.AutoConfig.StreamPage.DisconnectAccount + + 9 - - - - - - Qt::Horizontal + + 2 - - - 40 - 20 - + + 9 - - - - - - - - Basic.Settings.Stream.BandwidthTestMode - - - - - - - Basic.Settings.Stream.Custom.UseAuthentication - - - - - - - Basic.Settings.Stream.Custom.Username - - - authUsername - - - - - - - - - - Basic.Settings.Stream.Custom.Password - - - authPw - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QLineEdit::Password - - - - - - - Show - - - - - - - - - - - - - Basic.Settings.Stream.TTVAddon - - - twitchAddonDropdown - - - - - - - Basic.Settings.Stream.IgnoreRecommended - - - - - - - - - - Qt::RichText - - - true - - - - - - - - - Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced + + 9 - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Basic.AutoConfig.StreamPage.ConnectedAccount - - - - - - - - - PointingHandCursor - - - Basic.AutoConfig.StreamPage.ConnectAccount - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - + + + + Basic.Settings.Stream.TTVAddon + + + twitchAddonDropdown + + + + + + + + + + Qt::Horizontal + + + + 170 + 0 + + + + + + + + Basic.Settings.Stream.BandwidthTestMode + + + + + + + Basic.Settings.Stream.IgnoreRecommended + + + + + + + + + + Qt::RichText + + + true + + + + + + + + @@ -1585,6 +1846,25 @@ 0 + + + + notice + + + + + + Basic.Settings.MultitrackVideoDisabledSettings + + + Qt::AlignCenter + + + + + + diff --git a/UI/goliveapi-censoredjson.cpp b/UI/goliveapi-censoredjson.cpp new file mode 100644 index 000000000..fed481912 --- /dev/null +++ b/UI/goliveapi-censoredjson.cpp @@ -0,0 +1,89 @@ +#include "goliveapi-censoredjson.hpp" +#include +#include + +void censorRecurse(obs_data_t *); +void censorRecurseArray(obs_data_array_t *); + +void censorRecurse(obs_data_t *data) +{ + // if we found what we came to censor, censor it + const char *a = obs_data_get_string(data, "authentication"); + if (a && *a) { + obs_data_set_string(data, "authentication", "CENSORED"); + } + + // recurse to child objects and arrays + obs_data_item_t *item = obs_data_first(data); + for (; item != NULL; obs_data_item_next(&item)) { + enum obs_data_type typ = obs_data_item_gettype(item); + + if (typ == OBS_DATA_OBJECT) { + obs_data_t *child_data = obs_data_item_get_obj(item); + censorRecurse(child_data); + obs_data_release(child_data); + } else if (typ == OBS_DATA_ARRAY) { + obs_data_array_t *child_array = + obs_data_item_get_array(item); + censorRecurseArray(child_array); + obs_data_array_release(child_array); + } + } +} + +void censorRecurseArray(obs_data_array_t *array) +{ + const size_t sz = obs_data_array_count(array); + for (size_t i = 0; i < sz; i++) { + obs_data_t *item = obs_data_array_item(array, i); + censorRecurse(item); + obs_data_release(item); + } +} + +QString censoredJson(obs_data_t *data, bool pretty) +{ + if (!data) { + return ""; + } + + // Ugly clone via JSON write/read + const char *j = obs_data_get_json(data); + obs_data_t *clone = obs_data_create_from_json(j); + + // Censor our copy + censorRecurse(clone); + + // Turn our copy into JSON + QString s = pretty ? obs_data_get_json_pretty(clone) + : obs_data_get_json(clone); + + // Eliminate our copy + obs_data_release(clone); + + return s; +} + +using json = nlohmann::json; + +void censorRecurse(json &data) +{ + if (!data.is_structured()) + return; + + auto it = data.find("authentication"); + if (it != data.end() && it->is_string()) { + *it = "CENSORED"; + } + + for (auto &child : data) { + censorRecurse(child); + } +} + +QString censoredJson(json data, bool pretty) +{ + censorRecurse(data); + + return QString::fromStdString(data.dump(pretty ? 4 : -1)); +} diff --git a/UI/goliveapi-censoredjson.hpp b/UI/goliveapi-censoredjson.hpp new file mode 100644 index 000000000..20f141c7f --- /dev/null +++ b/UI/goliveapi-censoredjson.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +/** + * Returns the input serialized to JSON, but any non-empty "authorization" + * properties have their values replaced by "CENSORED". + */ +QString censoredJson(obs_data_t *data, bool pretty = false); +QString censoredJson(nlohmann::json data, bool pretty = false); diff --git a/UI/goliveapi-network.cpp b/UI/goliveapi-network.cpp new file mode 100644 index 000000000..34b751eb4 --- /dev/null +++ b/UI/goliveapi-network.cpp @@ -0,0 +1,146 @@ +#include "goliveapi-network.hpp" +#include "goliveapi-censoredjson.hpp" + +#include +#include +#include +#include "multitrack-video-error.hpp" + +#include +#include +#include +#include + +#include + +using json = nlohmann::json; + +Qt::ConnectionType BlockingConnectionTypeFor(QObject *object); + +void HandleGoLiveApiErrors(QWidget *parent, const json &raw_json, + const GoLiveApi::Config &config) +{ + using GoLiveApi::StatusResult; + + if (!config.status) + return; + + auto &status = *config.status; + if (status.result == StatusResult::Success) + return; + + auto warn_continue = [&](QString message) { + bool ret = false; + QMetaObject::invokeMethod( + parent, + [=] { + QMessageBox mb(parent); + mb.setIcon(QMessageBox::Warning); + mb.setWindowTitle(QTStr( + "ConfigDownload.WarningMessageTitle")); + mb.setTextFormat(Qt::RichText); + mb.setText( + message + + QTStr("FailedToStartStream.WarningRetry")); + mb.setStandardButtons( + QMessageBox::StandardButton::Yes | + QMessageBox::StandardButton::No); + return mb.exec() == + QMessageBox::StandardButton::No; + }, + BlockingConnectionTypeFor(parent), &ret); + if (ret) + throw MultitrackVideoError::cancel(); + }; + + auto missing_html = [] { + return QTStr("FailedToStartStream.StatusMissingHTML") + .toStdString(); + }; + + if (status.result == StatusResult::Unknown) { + return warn_continue( + QTStr("FailedToStartStream.WarningUnknownStatus") + .arg(raw_json["status"]["result"] + .dump() + .c_str())); + + } else if (status.result == StatusResult::Warning) { + if (config.encoder_configurations.empty()) { + throw MultitrackVideoError::warning( + status.html_en_us.value_or(missing_html()) + .c_str()); + } + + return warn_continue( + status.html_en_us.value_or(missing_html()).c_str()); + } else if (status.result == StatusResult::Error) { + throw MultitrackVideoError::critical( + status.html_en_us.value_or(missing_html()).c_str()); + } +} + +GoLiveApi::Config DownloadGoLiveConfig(QWidget *parent, QString url, + const GoLiveApi::PostData &post_data, + const QString &multitrack_video_name) +{ + json post_data_json = post_data; + blog(LOG_INFO, "Go live POST data: %s", + censoredJson(post_data_json).toUtf8().constData()); + + if (url.isEmpty()) + throw MultitrackVideoError::critical( + QTStr("FailedToStartStream.MissingConfigURL")); + + std::string encodeConfigText; + std::string libraryError; + + std::vector headers; + headers.push_back("Content-Type: application/json"); + bool encodeConfigDownloadedOk = GetRemoteFile( + url.toLocal8Bit(), encodeConfigText, + libraryError, // out params + nullptr, + nullptr, // out params (response code and content type) + "POST", post_data_json.dump().c_str(), headers, + nullptr, // signature + 5); // timeout in seconds + + if (!encodeConfigDownloadedOk) + throw MultitrackVideoError::warning( + QTStr("FailedToStartStream.ConfigRequestFailed") + .arg(url, libraryError.c_str())); + try { + auto data = json::parse(encodeConfigText); + blog(LOG_INFO, "Go live response data: %s", + censoredJson(data, true).toUtf8().constData()); + GoLiveApi::Config config = data; + HandleGoLiveApiErrors(parent, data, config); + return config; + + } catch (const json::exception &e) { + blog(LOG_INFO, "Failed to parse go live config: %s", e.what()); + throw MultitrackVideoError::warning( + QTStr("FailedToStartStream.FallbackToDefault") + .arg(multitrack_video_name)); + } +} + +QString MultitrackVideoAutoConfigURL(obs_service_t *service) +{ + static const QString url = [service]() -> QString { + auto args = qApp->arguments(); + for (int i = 0; i < args.length() - 1; i++) { + if (args[i] == "--config-url" && + args.length() > (i + 1)) { + return args[i + 1]; + } + } + OBSDataAutoRelease settings = obs_service_get_settings(service); + return obs_data_get_string( + settings, "multitrack_video_configuration_url"); + }(); + + blog(LOG_INFO, "Go live URL: %s", url.toUtf8().constData()); + return url; +} diff --git a/UI/goliveapi-network.hpp b/UI/goliveapi-network.hpp new file mode 100644 index 000000000..2fd3def43 --- /dev/null +++ b/UI/goliveapi-network.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +#include "models/multitrack-video.hpp" + +/** Returns either GO_LIVE_API_PRODUCTION_URL or a command line override. */ +QString MultitrackVideoAutoConfigURL(obs_service_t *service); + +class QWidget; + +GoLiveApi::Config DownloadGoLiveConfig(QWidget *parent, QString url, + const GoLiveApi::PostData &post_data, + const QString &multitrack_video_name); diff --git a/UI/goliveapi-postdata.cpp b/UI/goliveapi-postdata.cpp new file mode 100644 index 000000000..090fec75d --- /dev/null +++ b/UI/goliveapi-postdata.cpp @@ -0,0 +1,47 @@ +#include "goliveapi-postdata.hpp" + +#include + +#include "system-info.hpp" + +#include "models/multitrack-video.hpp" + +GoLiveApi::PostData +constructGoLivePost(QString streamKey, + const std::optional &maximum_aggregate_bitrate, + const std::optional &maximum_video_tracks, + bool vod_track_enabled) +{ + GoLiveApi::PostData post_data{}; + post_data.service = "IVS"; + post_data.schema_version = "2023-05-10"; + post_data.authentication = streamKey.toStdString(); + + system_info(post_data.capabilities); + + auto &client = post_data.capabilities.client; + + client.name = "obs-studio"; + client.version = obs_get_version_string(); + client.vod_track_audio = vod_track_enabled; + + obs_video_info ovi; + if (obs_get_video_info(&ovi)) { + client.width = ovi.output_width; + client.height = ovi.output_height; + client.fps_numerator = ovi.fps_num; + client.fps_denominator = ovi.fps_den; + + client.canvas_width = ovi.base_width; + client.canvas_height = ovi.base_height; + } + + auto &preferences = post_data.preferences; + if (maximum_aggregate_bitrate.has_value()) + preferences.maximum_aggregate_bitrate = + maximum_aggregate_bitrate.value(); + if (maximum_video_tracks.has_value()) + preferences.maximum_video_tracks = maximum_video_tracks.value(); + + return post_data; +} diff --git a/UI/goliveapi-postdata.hpp b/UI/goliveapi-postdata.hpp new file mode 100644 index 000000000..fbb671d9b --- /dev/null +++ b/UI/goliveapi-postdata.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include +#include "models/multitrack-video.hpp" + +GoLiveApi::PostData +constructGoLivePost(QString streamKey, + const std::optional &maximum_aggregate_bitrate, + const std::optional &maximum_video_tracks, + bool vod_track_enabled); diff --git a/UI/models/multitrack-video.hpp b/UI/models/multitrack-video.hpp new file mode 100644 index 000000000..2acccf87c --- /dev/null +++ b/UI/models/multitrack-video.hpp @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2024 Ruwen Hahn + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#pragma once + +#include +#include + +#include + +#include + +/* From whatsnew.hpp */ +#ifndef NLOHMANN_DEFINE_TYPE_INTRUSIVE +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type, ...) \ + friend void to_json(nlohmann::json &nlohmann_json_j, \ + const Type &nlohmann_json_t) \ + { \ + NLOHMANN_JSON_EXPAND( \ + NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) \ + } \ + friend void from_json(const nlohmann::json &nlohmann_json_j, \ + Type &nlohmann_json_t) \ + { \ + NLOHMANN_JSON_EXPAND( \ + NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) \ + } +#endif + +#ifndef NLOHMANN_JSON_FROM_WITH_DEFAULT +#define NLOHMANN_JSON_FROM_WITH_DEFAULT(v1) \ + nlohmann_json_t.v1 = \ + nlohmann_json_j.value(#v1, nlohmann_json_default_obj.v1); +#endif + +#ifndef NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Type, ...) \ + friend void to_json(nlohmann::json &nlohmann_json_j, \ + const Type &nlohmann_json_t) \ + { \ + NLOHMANN_JSON_EXPAND( \ + NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) \ + } \ + friend void from_json(const nlohmann::json &nlohmann_json_j, \ + Type &nlohmann_json_t) \ + { \ + Type nlohmann_json_default_obj; \ + NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE( \ + NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) \ + } + +#endif + +/* + * Support for (de-)serialising std::optional + * From https://github.com/nlohmann/json/issues/1749#issuecomment-1731266676 + * whatsnew.hpp's version doesn't seem to work here + */ +template struct nlohmann::adl_serializer> { + static void from_json(const json &j, std::optional &opt) + { + if (j.is_null()) { + opt = std::nullopt; + } else { + opt = j.get(); + } + } + static void to_json(json &json, std::optional t) + { + if (t) { + json = *t; + } else { + json = nullptr; + } + } +}; + +NLOHMANN_JSON_SERIALIZE_ENUM(obs_scale_type, + { + {OBS_SCALE_DISABLE, "OBS_SCALE_DISABLE"}, + {OBS_SCALE_POINT, "OBS_SCALE_POINT"}, + {OBS_SCALE_BICUBIC, "OBS_SCALE_BICUBIC"}, + {OBS_SCALE_BILINEAR, "OBS_SCALE_BILINEAR"}, + {OBS_SCALE_LANCZOS, "OBS_SCALE_LANCZOS"}, + {OBS_SCALE_AREA, "OBS_SCALE_AREA"}, + }) + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(media_frames_per_second, numerator, + denominator) + +namespace GoLiveApi { +using std::string; +using std::optional; +using json = nlohmann::json; + +struct Client { + string name = "obs-studio"; + string version; + bool vod_track_audio; + uint32_t width; + uint32_t height; + uint32_t fps_numerator; + uint32_t fps_denominator; + uint32_t canvas_width; + uint32_t canvas_height; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(Client, name, version, vod_track_audio, + width, height, fps_numerator, + fps_denominator, canvas_width, + canvas_height) +}; + +struct Cpu { + int32_t physical_cores; + int32_t logical_cores; + optional speed; + optional name; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(Cpu, physical_cores, logical_cores, + speed, name) +}; + +struct Memory { + uint64_t total; + uint64_t free; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(Memory, total, free) +}; + +struct Gpu { + string model; + uint32_t vendor_id; + uint32_t device_id; + uint64_t dedicated_video_memory; + uint64_t shared_system_memory; + string luid; + optional driver_version; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(Gpu, model, vendor_id, device_id, + dedicated_video_memory, + shared_system_memory, luid, + driver_version) +}; + +struct GamingFeatures { + optional game_bar_enabled; + optional game_dvr_allowed; + optional game_dvr_enabled; + optional game_dvr_bg_recording; + optional game_mode_enabled; + optional hags_enabled; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(GamingFeatures, game_bar_enabled, + game_dvr_allowed, game_dvr_enabled, + game_dvr_bg_recording, game_mode_enabled, + hags_enabled) +}; + +struct System { + string version; + string name; + int build; + string release; + int revision; + int bits; + bool arm; + bool armEmulation; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(System, version, name, build, release, + revision, bits, arm, armEmulation) +}; + +struct Capabilities { + Client client; + Cpu cpu; + Memory memory; + optional gaming_features; + System system; + optional> gpu; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(Capabilities, client, cpu, memory, + gaming_features, system, gpu) +}; + +struct Preferences { + optional maximum_aggregate_bitrate; + optional maximum_video_tracks; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(Preferences, maximum_aggregate_bitrate, + maximum_video_tracks) +}; + +struct PostData { + string service = "IVS"; + string schema_version = "2023-05-10"; + string authentication; + + Capabilities capabilities; + Preferences preferences; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(PostData, service, schema_version, + authentication, capabilities, + preferences) +}; + +// Config Response + +struct Meta { + string service; + string schema_version; + string config_id; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(Meta, service, schema_version, config_id) +}; + +enum struct StatusResult { + Unknown, + Success, + Warning, + Error, +}; + +NLOHMANN_JSON_SERIALIZE_ENUM(StatusResult, + { + {StatusResult::Unknown, nullptr}, + {StatusResult::Success, "success"}, + {StatusResult::Warning, "warning"}, + {StatusResult::Error, "error"}, + }) + +struct Status { + StatusResult result = StatusResult::Unknown; + optional html_en_us; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Status, result, html_en_us) +}; + +struct IngestEndpoint { + string protocol; + string url_template; + optional authentication; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(IngestEndpoint, protocol, + url_template, + authentication) +}; + +struct VideoEncoderConfiguration { + string type; + uint32_t width; + uint32_t height; + uint32_t bitrate; + optional framerate; + optional gpu_scale_type; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(VideoEncoderConfiguration, + type, width, height, + bitrate, framerate, + gpu_scale_type) +}; + +struct AudioEncoderConfiguration { + string codec; + uint32_t track_id; + uint32_t channels; + uint32_t bitrate; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(AudioEncoderConfiguration, codec, + track_id, channels, bitrate) +}; + +template struct EncoderConfiguration { + T config; + json data; + + friend void to_json(nlohmann::json &nlohmann_json_j, + const EncoderConfiguration &nlohmann_json_t) + { + nlohmann_json_j = nlohmann_json_t.data; + to_json(nlohmann_json_j, nlohmann_json_t.config); + } + friend void from_json(const nlohmann::json &nlohmann_json_j, + EncoderConfiguration &nlohmann_json_t) + { + nlohmann_json_t.data = nlohmann_json_j; + nlohmann_json_j.get_to(nlohmann_json_t.config); + } +}; + +struct AudioConfigurations { + std::vector> live; + std::vector> vod; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(AudioConfigurations, live, + vod) +}; + +struct Config { + Meta meta; + optional status; + std::vector ingest_endpoints; + std::vector> + encoder_configurations; + AudioConfigurations audio_configurations; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, meta, status, + ingest_endpoints, + encoder_configurations, + audio_configurations) +}; +} // namespace GoLiveApi diff --git a/UI/multitrack-video-error.cpp b/UI/multitrack-video-error.cpp new file mode 100644 index 000000000..036a80f89 --- /dev/null +++ b/UI/multitrack-video-error.cpp @@ -0,0 +1,46 @@ +#include "multitrack-video-error.hpp" + +#include +#include "obs-app.hpp" + +MultitrackVideoError MultitrackVideoError::critical(QString error) +{ + return {Type::Critical, error}; +} + +MultitrackVideoError MultitrackVideoError::warning(QString error) +{ + return {Type::Warning, error}; +} + +MultitrackVideoError MultitrackVideoError::cancel() +{ + return {Type::Cancel, {}}; +} + +bool MultitrackVideoError::ShowDialog( + QWidget *parent, const QString &multitrack_video_name) const +{ + QMessageBox mb(parent); + mb.setTextFormat(Qt::RichText); + mb.setWindowTitle(QTStr("Output.StartStreamFailed")); + + if (type == Type::Warning) { + mb.setText( + error + + QTStr("FailedToStartStream.WarningRetryNonMultitrackVideo") + .arg(multitrack_video_name)); + mb.setIcon(QMessageBox::Warning); + mb.setStandardButtons(QMessageBox::StandardButton::Yes | + QMessageBox::StandardButton::No); + return mb.exec() == QMessageBox::StandardButton::Yes; + } else if (type == Type::Critical) { + mb.setText(error); + mb.setIcon(QMessageBox::Critical); + mb.setStandardButtons( + QMessageBox::StandardButton::Ok); // cannot continue + mb.exec(); + } + + return false; +} diff --git a/UI/multitrack-video-error.hpp b/UI/multitrack-video-error.hpp new file mode 100644 index 000000000..cbffb4254 --- /dev/null +++ b/UI/multitrack-video-error.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + +class QWidget; + +struct MultitrackVideoError { + static MultitrackVideoError critical(QString error); + static MultitrackVideoError warning(QString error); + static MultitrackVideoError cancel(); + + bool ShowDialog(QWidget *parent, + const QString &multitrack_video_name) const; + + enum struct Type { + Critical, + Warning, + Cancel, + }; + + const Type type; + const QString error; +}; diff --git a/UI/multitrack-video-output.cpp b/UI/multitrack-video-output.cpp new file mode 100644 index 000000000..2acc64c0c --- /dev/null +++ b/UI/multitrack-video-output.cpp @@ -0,0 +1,950 @@ +#include "multitrack-video-output.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "system-info.hpp" +#include "goliveapi-postdata.hpp" +#include "goliveapi-network.hpp" +#include "multitrack-video-error.hpp" +#include "qt-helpers.hpp" +#include "models/multitrack-video.hpp" + +Qt::ConnectionType BlockingConnectionTypeFor(QObject *object) +{ + return object->thread() == QThread::currentThread() + ? Qt::DirectConnection + : Qt::BlockingQueuedConnection; +} + +bool MultitrackVideoDeveloperModeEnabled() +{ + static bool developer_mode = [] { + auto args = qApp->arguments(); + for (const auto &arg : args) { + if (arg == "--enable-multitrack-video-dev") { + return true; + } + } + return false; + }(); + return developer_mode; +} + +static OBSServiceAutoRelease +create_service(const GoLiveApi::Config &go_live_config, + const std::optional &rtmp_url, + const QString &in_stream_key) +{ + const char *url = nullptr; + QString stream_key = in_stream_key; + + const auto &ingest_endpoints = go_live_config.ingest_endpoints; + + for (auto &endpoint : ingest_endpoints) { + if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4)) + continue; + + url = endpoint.url_template.c_str(); + if (endpoint.authentication && + !endpoint.authentication->empty()) { + blog(LOG_INFO, + "Using stream key supplied by autoconfig"); + stream_key = QString::fromStdString( + *endpoint.authentication); + } + break; + } + + if (rtmp_url.has_value()) { + // Despite being set by user, it was set to a "" + if (rtmp_url->empty()) { + throw MultitrackVideoError::warning(QTStr( + "FailedToStartStream.NoCustomRTMPURLInSettings")); + } + + url = rtmp_url->c_str(); + blog(LOG_INFO, "Using custom RTMP URL: '%s'", url); + } else { + if (!url) { + blog(LOG_ERROR, "No RTMP URL in go live config"); + throw MultitrackVideoError::warning( + QTStr("FailedToStartStream.NoRTMPURLInConfig")); + } + + blog(LOG_INFO, "Using URL template: '%s'", url); + } + + DStr str; + dstr_cat(str, url); + + // dstr_find does not protect against null, and dstr_cat will + // not initialize str if cat'ing with a null url + if (!dstr_is_empty(str)) { + auto found = dstr_find(str, "/{stream_key}"); + if (found) + dstr_remove(str, found - str->array, + str->len - (found - str->array)); + } + + QUrl parsed_url{url}; + QUrlQuery parsed_query{parsed_url}; + + if (!go_live_config.meta.config_id.empty()) { + parsed_query.addQueryItem( + "obsConfigId", + QString::fromStdString(go_live_config.meta.config_id)); + } + + auto key_with_param = stream_key; + if (!parsed_query.isEmpty()) + key_with_param += "?" + parsed_query.toString(); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "server", str->array); + obs_data_set_string(settings, "key", + key_with_param.toUtf8().constData()); + + auto service = obs_service_create( + "rtmp_custom", "multitrack video service", settings, nullptr); + + if (!service) { + blog(LOG_WARNING, "Failed to create multitrack video service"); + throw MultitrackVideoError::warning(QTStr( + "FailedToStartStream.FailedToCreateMultitrackVideoService")); + } + + return service; +} + +static void ensure_directory_exists(std::string &path) +{ + replace(path.begin(), path.end(), '\\', '/'); + + size_t last = path.rfind('/'); + if (last == std::string::npos) + return; + + std::string directory = path.substr(0, last); + os_mkdirs(directory.c_str()); +} + +std::string GetOutputFilename(const std::string &path, const char *format) +{ + std::string strPath; + strPath += path; + + char lastChar = strPath.back(); + if (lastChar != '/' && lastChar != '\\') + strPath += "/"; + + strPath += BPtr{ + os_generate_formatted_filename("flv", false, format)}; + ensure_directory_exists(strPath); + + return strPath; +} + +static OBSOutputAutoRelease create_output() +{ + OBSOutputAutoRelease output = obs_output_create( + "rtmp_output", "rtmp multitrack video", nullptr, nullptr); + + if (!output) { + blog(LOG_ERROR, + "Failed to create multitrack video rtmp output"); + throw MultitrackVideoError::warning(QTStr( + "FailedToStartStream.FailedToCreateMultitrackVideoOutput")); + } + + return output; +} + +static OBSOutputAutoRelease create_recording_output(obs_data_t *settings) +{ + OBSOutputAutoRelease output = obs_output_create( + "flv_output", "flv multitrack video", settings, nullptr); + + if (!output) + blog(LOG_ERROR, "Failed to create multitrack video flv output"); + + return output; +} + +static void adjust_video_encoder_scaling( + const obs_video_info &ovi, obs_encoder_t *video_encoder, + const GoLiveApi::VideoEncoderConfiguration &encoder_config, + size_t encoder_index) +{ + auto requested_width = encoder_config.width; + auto requested_height = encoder_config.height; + + if (ovi.output_width == requested_width || + ovi.output_height == requested_height) + return; + + if (ovi.base_width < requested_width || + ovi.base_height < requested_height) { + blog(LOG_WARNING, + "Requested resolution exceeds canvas/available resolution for encoder %zu: %" PRIu32 + "x%" PRIu32 " > %" PRIu32 "x%" PRIu32, + encoder_index, requested_width, requested_height, + ovi.base_width, ovi.base_height); + } + + obs_encoder_set_scaled_size(video_encoder, requested_width, + requested_height); + obs_encoder_set_gpu_scale_type( + video_encoder, + encoder_config.gpu_scale_type.value_or(OBS_SCALE_BICUBIC)); +} + +static uint32_t closest_divisor(const obs_video_info &ovi, + const media_frames_per_second &target_fps) +{ + auto target = (uint64_t)target_fps.numerator * ovi.fps_den; + auto source = (uint64_t)ovi.fps_num * target_fps.denominator; + return std::max(1u, static_cast(source / target)); +} + +static void adjust_encoder_frame_rate_divisor( + const obs_video_info &ovi, obs_encoder_t *video_encoder, + const GoLiveApi::VideoEncoderConfiguration &encoder_config, + const size_t encoder_index) +{ + if (!encoder_config.framerate) { + blog(LOG_WARNING, "`framerate` not specified for encoder %zu", + encoder_index); + return; + } + media_frames_per_second requested_fps = *encoder_config.framerate; + + if (ovi.fps_num == requested_fps.numerator && + ovi.fps_den == requested_fps.denominator) + return; + + auto divisor = closest_divisor(ovi, requested_fps); + if (divisor <= 1) + return; + + blog(LOG_INFO, "Setting frame rate divisor to %u for encoder %zu", + divisor, encoder_index); + obs_encoder_set_frame_rate_divisor(video_encoder, divisor); +} + +static const std::vector &get_available_encoders() +{ + // encoders are currently only registered during startup, so keeping + // a static vector around shouldn't be a problem + static std::vector available_encoders = [] { + std::vector available_encoders; + for (size_t i = 0;; i++) { + const char *id = nullptr; + if (!obs_enum_encoder_types(i, &id)) + break; + available_encoders.push_back(id); + } + return available_encoders; + }(); + return available_encoders; +} + +static bool encoder_available(const char *type) +{ + auto &encoders = get_available_encoders(); + return std::find_if(std::begin(encoders), std::end(encoders), + [=](const char *encoder) { + return strcmp(type, encoder) == 0; + }) != std::end(encoders); +} + +static OBSEncoderAutoRelease create_video_encoder( + DStr &name_buffer, size_t encoder_index, + const GoLiveApi::EncoderConfiguration< + GoLiveApi::VideoEncoderConfiguration> &encoder_config) +{ + auto encoder_type = encoder_config.config.type.c_str(); + if (!encoder_available(encoder_type)) { + blog(LOG_ERROR, "Encoder type '%s' not available", + encoder_type); + throw MultitrackVideoError::warning( + QTStr("FailedToStartStream.EncoderNotAvailable") + .arg(encoder_type)); + } + + dstr_printf(name_buffer, "multitrack video video encoder %zu", + encoder_index); + + OBSDataAutoRelease encoder_settings = + obs_data_create_from_json(encoder_config.data.dump().c_str()); + obs_data_set_bool(encoder_settings, "disable_scenecut", true); + + OBSEncoderAutoRelease video_encoder = obs_video_encoder_create( + encoder_type, name_buffer, encoder_settings, nullptr); + if (!video_encoder) { + blog(LOG_ERROR, "Failed to create video encoder '%s'", + name_buffer->array); + throw MultitrackVideoError::warning( + QTStr("FailedToStartStream.FailedToCreateVideoEncoder") + .arg(name_buffer->array, encoder_type)); + } + obs_encoder_set_video(video_encoder, obs_get_video()); + + obs_video_info ovi; + if (!obs_get_video_info(&ovi)) { + blog(LOG_WARNING, + "Failed to get obs_video_info while creating encoder %zu", + encoder_index); + throw MultitrackVideoError::warning( + QTStr("FailedToStartStream.FailedToGetOBSVideoInfo") + .arg(name_buffer->array, encoder_type)); + } + + adjust_video_encoder_scaling(ovi, video_encoder, encoder_config.config, + encoder_index); + adjust_encoder_frame_rate_divisor(ovi, video_encoder, + encoder_config.config, encoder_index); + + return video_encoder; +} + +static OBSEncoderAutoRelease create_audio_encoder(const char *name, + const char *audio_encoder_id, + uint32_t audio_bitrate, + size_t mixer_idx) +{ + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_int(settings, "bitrate", audio_bitrate); + + OBSEncoderAutoRelease audio_encoder = obs_audio_encoder_create( + audio_encoder_id, name, settings, mixer_idx, nullptr); + if (!audio_encoder) { + blog(LOG_ERROR, "Failed to create audio encoder"); + throw MultitrackVideoError::warning(QTStr( + "FailedToStartStream.FailedToCreateAudioEncoder")); + } + obs_encoder_set_audio(audio_encoder, obs_get_audio()); + return audio_encoder; +} + +struct OBSOutputs { + OBSOutputAutoRelease output, recording_output; +}; + +static OBSOutputs +SetupOBSOutput(obs_data_t *dump_stream_to_file_config, + const GoLiveApi::Config &go_live_config, + std::vector &audio_encoders, + std::vector &video_encoders, + const char *audio_encoder_id, + std::optional vod_track_mixer); +static void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self, + obs_output_t *output, OBSSignal &start, + OBSSignal &stop, OBSSignal &deactivate); + +void MultitrackVideoOutput::PrepareStreaming( + QWidget *parent, const char *service_name, obs_service_t *service, + const std::optional &rtmp_url, const QString &stream_key, + const char *audio_encoder_id, + std::optional maximum_aggregate_bitrate, + std::optional maximum_video_tracks, + std::optional custom_config, + obs_data_t *dump_stream_to_file_config, + std::optional vod_track_mixer) +{ + { + const std::lock_guard current_lock{current_mutex}; + const std::lock_guard current_stream_dump_lock{ + current_stream_dump_mutex}; + if (current || current_stream_dump) { + blog(LOG_WARNING, + "Tried to prepare multitrack video output while it's already active"); + return; + } + } + std::optional go_live_config; + std::optional custom; + bool is_custom_config = custom_config.has_value(); + auto auto_config_url = MultitrackVideoAutoConfigURL(service); + + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + auto multitrack_video_name = + QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, + "ertmp_multitrack_video_name")) { + multitrack_video_name = obs_data_get_string( + service_settings, "ertmp_multitrack_video_name"); + } + + auto auto_config_url_data = auto_config_url.toUtf8(); + + DStr vod_track_info_storage; + if (vod_track_mixer.has_value()) + dstr_printf(vod_track_info_storage, "Yes (mixer: %zu)", + vod_track_mixer.value()); + + blog(LOG_INFO, + "Preparing enhanced broadcasting stream for:\n" + " custom config: %s\n" + " config url: %s\n" + " settings:\n" + " service: %s\n" + " max aggregate bitrate: %s (%" PRIu32 ")\n" + " max video tracks: %s (%" PRIu32 ")\n" + " custom rtmp url: %s ('%s')\n" + " vod track: %s", + is_custom_config ? "Yes" : "No", + !auto_config_url.isEmpty() ? auto_config_url_data.constData() + : "(null)", + service_name, + maximum_aggregate_bitrate.has_value() ? "Set" : "Auto", + maximum_aggregate_bitrate.value_or(0), + maximum_video_tracks.has_value() ? "Set" : "Auto", + maximum_video_tracks.value_or(0), + rtmp_url.has_value() ? "Yes" : "No", + rtmp_url.has_value() ? rtmp_url->c_str() : "", + vod_track_info_storage->array ? vod_track_info_storage->array + : "No"); + + const bool custom_config_only = + auto_config_url.isEmpty() && + MultitrackVideoDeveloperModeEnabled() && + custom_config.has_value() && + strcmp(obs_service_get_id(service), "rtmp_custom") == 0; + + if (!custom_config_only) { + auto go_live_post = constructGoLivePost( + stream_key, maximum_aggregate_bitrate, + maximum_video_tracks, vod_track_mixer.has_value()); + + go_live_config = DownloadGoLiveConfig(parent, auto_config_url, + go_live_post, + multitrack_video_name); + } + + if (custom_config.has_value()) { + GoLiveApi::Config parsed_custom; + try { + parsed_custom = nlohmann::json::parse(*custom_config); + } catch (const nlohmann::json::exception &exception) { + blog(LOG_WARNING, "Failed to parse custom config: %s", + exception.what()); + throw MultitrackVideoError::critical(QTStr( + "FailedToStartStream.InvalidCustomConfig")); + } + + // copy unique ID from go live request + if (go_live_config.has_value()) { + parsed_custom.meta.config_id = + go_live_config->meta.config_id; + blog(LOG_INFO, + "Using config_id from go live config with custom config: %s", + parsed_custom.meta.config_id.c_str()); + } + + nlohmann::json custom_data = parsed_custom; + blog(LOG_INFO, "Using custom go live config: %s", + custom_data.dump(4).c_str()); + + custom.emplace(std::move(parsed_custom)); + } + + if (go_live_config.has_value()) { + blog(LOG_INFO, "Enhanced broadcasting config_id: '%s'", + go_live_config->meta.config_id.c_str()); + } + + if (!go_live_config && !custom) { + blog(LOG_ERROR, + "MultitrackVideoOutput: no config set, this should never happen"); + throw MultitrackVideoError::warning( + QTStr("FailedToStartStream.NoConfig")); + } + + const auto &output_config = custom ? *custom : *go_live_config; + const auto &service_config = go_live_config ? *go_live_config : *custom; + + auto audio_encoders = std::vector(); + auto video_encoders = std::vector(); + auto outputs = SetupOBSOutput(dump_stream_to_file_config, output_config, + audio_encoders, video_encoders, + audio_encoder_id, vod_track_mixer); + auto output = std::move(outputs.output); + auto recording_output = std::move(outputs.recording_output); + if (!output) + throw MultitrackVideoError::warning( + QTStr("FailedToStartStream.FallbackToDefault") + .arg(multitrack_video_name)); + + auto multitrack_video_service = + create_service(service_config, rtmp_url, stream_key); + if (!multitrack_video_service) + throw MultitrackVideoError::warning( + QTStr("FailedToStartStream.FallbackToDefault") + .arg(multitrack_video_name)); + + obs_output_set_service(output, multitrack_video_service); + + OBSSignal start_streaming; + OBSSignal stop_streaming; + OBSSignal deactivate_stream; + SetupSignalHandlers(false, this, output, start_streaming, + stop_streaming, deactivate_stream); + + if (dump_stream_to_file_config && recording_output) { + OBSSignal start_recording; + OBSSignal stop_recording; + OBSSignal deactivate_recording; + SetupSignalHandlers(true, this, recording_output, + start_recording, stop_recording, + deactivate_recording); + + decltype(video_encoders) recording_video_encoders; + recording_video_encoders.reserve(video_encoders.size()); + for (auto &encoder : video_encoders) { + recording_video_encoders.emplace_back( + obs_encoder_get_ref(encoder)); + } + + decltype(audio_encoders) recording_audio_encoders; + recording_audio_encoders.reserve(audio_encoders.size()); + for (auto &encoder : audio_encoders) { + recording_audio_encoders.emplace_back( + obs_encoder_get_ref(encoder)); + } + + { + const std::lock_guard current_stream_dump_lock{ + current_stream_dump_mutex}; + current_stream_dump.emplace(OBSOutputObjects{ + std::move(recording_output), + std::move(recording_video_encoders), + std::move(recording_audio_encoders), + nullptr, + std::move(start_recording), + std::move(stop_recording), + std::move(deactivate_recording), + }); + } + } + + const std::lock_guard current_lock{current_mutex}; + current.emplace(OBSOutputObjects{ + std::move(output), + std::move(video_encoders), + std::move(audio_encoders), + std::move(multitrack_video_service), + std::move(start_streaming), + std::move(stop_streaming), + std::move(deactivate_stream), + }); +} + +signal_handler_t *MultitrackVideoOutput::StreamingSignalHandler() +{ + const std::lock_guard current_lock{current_mutex}; + return current.has_value() + ? obs_output_get_signal_handler(current->output_) + : nullptr; +} + +void MultitrackVideoOutput::StartedStreaming() +{ + OBSOutputAutoRelease dump_output; + { + const std::lock_guard current_stream_dump_lock{ + current_stream_dump_mutex}; + if (current_stream_dump && current_stream_dump->output_) { + dump_output = obs_output_get_ref( + current_stream_dump->output_); + } + } + + if (!dump_output) + return; + + auto result = obs_output_start(dump_output); + blog(LOG_INFO, "MultitrackVideoOutput: starting recording%s", + result ? "" : " failed"); +} + +void MultitrackVideoOutput::StopStreaming() +{ + OBSOutputAutoRelease current_output; + { + const std::lock_guard current_lock{current_mutex}; + if (current && current->output_) + current_output = obs_output_get_ref(current->output_); + } + if (current_output) + obs_output_stop(current_output); + + OBSOutputAutoRelease dump_output; + { + const std::lock_guard current_stream_dump_lock{ + current_stream_dump_mutex}; + if (current_stream_dump && current_stream_dump->output_) + dump_output = obs_output_get_ref( + current_stream_dump->output_); + } + if (dump_output) + obs_output_stop(dump_output); +} + +bool MultitrackVideoOutput::HandleIncompatibleSettings( + QWidget *parent, config_t *config, obs_service_t *service, + bool &useDelay, bool &enableNewSocketLoop, bool &enableDynBitrate) +{ + QString incompatible_settings; + QString where_to_disable; + QString incompatible_settings_list; + + size_t num = 1; + + auto check_setting = [&](bool setting, const char *name, + const char *section) { + if (!setting) + return; + + incompatible_settings += + QString(" %1. %2\n").arg(num).arg(QTStr(name)); + + where_to_disable += + QString(" %1. [%2 > %3 > %4]\n") + .arg(num) + .arg(QTStr("Settings")) + .arg(QTStr("Basic.Settings.Advanced")) + .arg(QTStr(section)); + + incompatible_settings_list += QString("%1, ").arg(name); + + num += 1; + }; + + check_setting(useDelay, "Basic.Settings.Advanced.StreamDelay", + "Basic.Settings.Advanced.StreamDelay"); +#ifdef _WIN32 + check_setting(enableNewSocketLoop, + "Basic.Settings.Advanced.Network.EnableNewSocketLoop", + "Basic.Settings.Advanced.Network"); +#endif + check_setting(enableDynBitrate, + "Basic.Settings.Output.DynamicBitrate.Beta", + "Basic.Settings.Advanced.Network"); + + if (incompatible_settings.isEmpty()) + return true; + + OBSDataAutoRelease service_settings = obs_service_get_settings(service); + + QMessageBox mb(parent); + mb.setIcon(QMessageBox::Critical); + mb.setWindowTitle(QTStr("MultitrackVideo.IncompatibleSettings.Title")); + mb.setText( + QString(QTStr("MultitrackVideo.IncompatibleSettings.Text")) + .arg(obs_data_get_string(service_settings, + "ertmp_multitrack_video_name")) + .arg(incompatible_settings) + .arg(where_to_disable)); + auto this_stream = mb.addButton( + QTStr("MultitrackVideo.IncompatibleSettings.DisableAndStartStreaming"), + QMessageBox::AcceptRole); + auto all_streams = mb.addButton( + QString(QTStr( + "MultitrackVideo.IncompatibleSettings.UpdateAndStartStreaming")), + QMessageBox::AcceptRole); + mb.setStandardButtons(QMessageBox::StandardButton::Cancel); + + mb.exec(); + + const char *action = "cancel"; + if (mb.clickedButton() == this_stream) { + action = "DisableAndStartStreaming"; + } else if (mb.clickedButton() == all_streams) { + action = "UpdateAndStartStreaming"; + } + + blog(LOG_INFO, + "MultitrackVideoOutput: attempted to start stream with incompatible" + "settings (%s); action taken: %s", + incompatible_settings_list.toUtf8().constData(), action); + + if (mb.clickedButton() == this_stream || + mb.clickedButton() == all_streams) { + useDelay = false; + enableNewSocketLoop = false; + enableDynBitrate = false; + useDelay = false; + enableNewSocketLoop = false; + enableDynBitrate = false; + + if (mb.clickedButton() == all_streams) { + config_set_bool(config, "Output", "DelayEnable", false); +#ifdef _WIN32 + config_set_bool(config, "Output", "NewSocketLoopEnable", + false); +#endif + config_set_bool(config, "Output", "DynamicBitrate", + false); + } + + return true; + } + + return false; +} + +static bool +create_video_encoders(const GoLiveApi::Config &go_live_config, + std::vector &video_encoders, + obs_output_t *output, obs_output_t *recording_output) +{ + DStr video_encoder_name_buffer; + obs_encoder_t *first_encoder = nullptr; + if (go_live_config.encoder_configurations.empty()) { + blog(LOG_WARNING, + "MultitrackVideoOutput: Missing video encoder configurations"); + throw MultitrackVideoError::warning( + QTStr("FailedToStartStream.MissingEncoderConfigs")); + } + + for (size_t i = 0; i < go_live_config.encoder_configurations.size(); + i++) { + auto encoder = create_video_encoder( + video_encoder_name_buffer, i, + go_live_config.encoder_configurations[i]); + if (!encoder) + return false; + + if (!first_encoder) + first_encoder = encoder; + else + obs_encoder_group_keyframe_aligned_encoders( + first_encoder, encoder); + + obs_output_set_video_encoder2(output, encoder, i); + if (recording_output) + obs_output_set_video_encoder2(recording_output, encoder, + i); + video_encoders.emplace_back(std::move(encoder)); + } + + return true; +} + +static void +create_audio_encoders(const GoLiveApi::Config &go_live_config, + std::vector &audio_encoders, + obs_output_t *output, obs_output_t *recording_output, + const char *audio_encoder_id, + std::optional vod_track_mixer) +{ + using encoder_configs_type = + decltype(go_live_config.audio_configurations.live); + DStr encoder_name_buffer; + size_t output_encoder_index = 0; + + auto create_encoders = [&](const char *name_prefix, + const encoder_configs_type &configs, + size_t mixer_idx) { + if (configs.empty()) { + blog(LOG_WARNING, + "MultitrackVideoOutput: Missing audio encoder configurations (for '%s')", + name_prefix); + throw MultitrackVideoError::warning(QTStr( + "FailedToStartStream.MissingEncoderConfigs")); + } + + for (size_t i = 0; i < configs.size(); i++) { + dstr_printf(encoder_name_buffer, "%s %zu", name_prefix, + i); + OBSEncoderAutoRelease audio_encoder = + create_audio_encoder(encoder_name_buffer->array, + audio_encoder_id, + configs[i].config.bitrate, + mixer_idx); + obs_output_set_audio_encoder(output, audio_encoder, + output_encoder_index); + if (recording_output) + obs_output_set_audio_encoder( + recording_output, audio_encoder, + output_encoder_index); + output_encoder_index += 1; + audio_encoders.emplace_back(std::move(audio_encoder)); + } + }; + + create_encoders("multitrack video live audio", + go_live_config.audio_configurations.live, 0); + + if (!vod_track_mixer.has_value()) + return; + + create_encoders("multitrack video vod audio", + go_live_config.audio_configurations.vod, + *vod_track_mixer); + + return; +} + +static OBSOutputs +SetupOBSOutput(obs_data_t *dump_stream_to_file_config, + const GoLiveApi::Config &go_live_config, + std::vector &audio_encoders, + std::vector &video_encoders, + const char *audio_encoder_id, + std::optional vod_track_mixer) +{ + + auto output = create_output(); + OBSOutputAutoRelease recording_output; + if (dump_stream_to_file_config) + recording_output = + create_recording_output(dump_stream_to_file_config); + + if (!create_video_encoders(go_live_config, video_encoders, output, + recording_output)) + return {nullptr, nullptr}; + + create_audio_encoders(go_live_config, audio_encoders, output, + recording_output, audio_encoder_id, + vod_track_mixer); + + return {std::move(output), std::move(recording_output)}; +} + +void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self, + obs_output_t *output, OBSSignal &start, + OBSSignal &stop, OBSSignal &deactivate) +{ + auto handler = obs_output_get_signal_handler(output); + + if (recording) + start.Connect(handler, "start", RecordingStartHandler, self); + + stop.Connect(handler, "stop", + !recording ? StreamStopHandler : RecordingStopHandler, + self); + + deactivate.Connect(handler, "deactivate", + !recording ? StreamDeactivateHandler + : RecordingDeactivateHandler, + self); +} + +std::optional +MultitrackVideoOutput::take_current() +{ + const std::lock_guard current_lock{current_mutex}; + auto val = std::move(current); + current.reset(); + return val; +} + +std::optional +MultitrackVideoOutput::take_current_stream_dump() +{ + const std::lock_guard current_stream_dump_lock{ + current_stream_dump_mutex}; + auto val = std::move(current_stream_dump); + current_stream_dump.reset(); + return val; +} + +void MultitrackVideoOutput::ReleaseOnMainThread( + std::optional objects) +{ + + if (!objects.has_value()) + return; + + QMetaObject::invokeMethod( + QApplication::instance()->thread(), + [objects = std::move(objects)] {}, Qt::QueuedConnection); +} + +void StreamStopHandler(void *arg, calldata_t *params) +{ + auto self = static_cast(arg); + + OBSOutputAutoRelease stream_dump_output; + { + const std::lock_guard current_stream_dump_lock{ + self->current_stream_dump_mutex}; + if (self->current_stream_dump && + self->current_stream_dump->output_) + stream_dump_output = obs_output_get_ref( + self->current_stream_dump->output_); + } + if (stream_dump_output) + obs_output_stop(stream_dump_output); + + if (obs_output_active(static_cast( + calldata_ptr(params, "output")))) + return; + + MultitrackVideoOutput::ReleaseOnMainThread(self->take_current()); +} + +void StreamDeactivateHandler(void *arg, calldata_t *params) +{ + auto self = static_cast(arg); + + if (obs_output_reconnecting(static_cast( + calldata_ptr(params, "output")))) + return; + + MultitrackVideoOutput::ReleaseOnMainThread(self->take_current()); +} + +void RecordingStartHandler(void * /* arg */, calldata_t * /* data */) +{ + blog(LOG_INFO, "MultitrackVideoOutput: recording started"); +} + +void RecordingStopHandler(void *arg, calldata_t *params) +{ + auto self = static_cast(arg); + blog(LOG_INFO, "MultitrackVideoOutput: recording stopped"); + + if (obs_output_active(static_cast( + calldata_ptr(params, "output")))) + return; + + MultitrackVideoOutput::ReleaseOnMainThread( + self->take_current_stream_dump()); +} + +void RecordingDeactivateHandler(void *arg, calldata_t * /*data*/) +{ + auto self = static_cast(arg); + MultitrackVideoOutput::ReleaseOnMainThread( + self->take_current_stream_dump()); +} diff --git a/UI/multitrack-video-output.hpp b/UI/multitrack-video-output.hpp new file mode 100644 index 000000000..586432429 --- /dev/null +++ b/UI/multitrack-video-output.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#define NOMINMAX + +class QString; + +void StreamStopHandler(void *arg, calldata_t *data); +void StreamDeactivateHandler(void *arg, calldata_t *data); + +void RecordingStartHandler(void *arg, calldata_t *data); +void RecordingStopHandler(void *arg, calldata_t *data); +void RecordingDeactivateHandler(void *arg, calldata_t *data); + +bool MultitrackVideoDeveloperModeEnabled(); + +struct MultitrackVideoOutput { +public: + void PrepareStreaming(QWidget *parent, const char *service_name, + obs_service_t *service, + const std::optional &rtmp_url, + const QString &stream_key, + const char *audio_encoder_id, + std::optional maximum_aggregate_bitrate, + std::optional maximum_video_tracks, + std::optional custom_config, + obs_data_t *dump_stream_to_file_config, + std::optional vod_track_mixer); + signal_handler_t *StreamingSignalHandler(); + void StartedStreaming(); + void StopStreaming(); + bool HandleIncompatibleSettings(QWidget *parent, config_t *config, + obs_service_t *service, bool &useDelay, + bool &enableNewSocketLoop, + bool &enableDynBitrate); + + OBSOutputAutoRelease StreamingOutput() + { + const std::lock_guard current_lock{current_mutex}; + return current ? obs_output_get_ref(current->output_) : nullptr; + } + +private: + struct OBSOutputObjects { + OBSOutputAutoRelease output_; + std::vector video_encoders_; + std::vector audio_encoders_; + OBSServiceAutoRelease multitrack_video_service_; + OBSSignal start_signal, stop_signal, deactivate_signal; + }; + + std::optional take_current(); + std::optional take_current_stream_dump(); + + static void + ReleaseOnMainThread(std::optional objects); + + std::mutex current_mutex; + std::optional current; + + std::mutex current_stream_dump_mutex; + std::optional current_stream_dump; + + friend void StreamStopHandler(void *arg, calldata_t *data); + friend void StreamDeactivateHandler(void *arg, calldata_t *data); + friend void RecordingStartHandler(void *arg, calldata_t *data); + friend void RecordingStopHandler(void *arg, calldata_t *data); + friend void RecordingDeactivateHandler(void *arg, calldata_t *data); +}; diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index 853630a02..c1ba6d6a3 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -386,7 +386,7 @@ static inline bool too_many_repeated_entries(fstream &logFile, const char *msg, static void do_log(int log_level, const char *msg, va_list args, void *param) { fstream &logFile = *static_cast(param); - char str[4096]; + char str[8192]; #ifndef _WIN32 va_list args2; diff --git a/UI/properties-view.cpp b/UI/properties-view.cpp index d7d5b3787..1f9658d06 100644 --- a/UI/properties-view.cpp +++ b/UI/properties-view.cpp @@ -245,6 +245,13 @@ OBSPropertiesView::OBSPropertiesView(OBSData settings_, const char *type_, Qt::QueuedConnection); } +void OBSPropertiesView::SetDisabled(bool disabled) +{ + for (auto child : findChildren()) { + child->setDisabled(disabled); + } +} + void OBSPropertiesView::resizeEvent(QResizeEvent *event) { emit PropertiesResized(); diff --git a/UI/properties-view.hpp b/UI/properties-view.hpp index 06ce853f0..42d9b36b8 100644 --- a/UI/properties-view.hpp +++ b/UI/properties-view.hpp @@ -208,6 +208,8 @@ public: RefreshProperties(); } + void SetDisabled(bool disabled); + #define Def_IsObject(type) \ inline bool IsObject(obs_##type##_t *type) const \ { \ diff --git a/UI/qt-helpers.cpp b/UI/qt-helpers.cpp new file mode 100644 index 000000000..ad76bd5a1 --- /dev/null +++ b/UI/qt-helpers.cpp @@ -0,0 +1,10 @@ +#include "qt-helpers.hpp" + +QFuture CreateFuture() +{ + QPromise promise; + auto future = promise.future(); + promise.start(); + promise.finish(); + return future; +} diff --git a/UI/qt-helpers.hpp b/UI/qt-helpers.hpp new file mode 100644 index 000000000..c0d7328c0 --- /dev/null +++ b/UI/qt-helpers.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +template struct FutureHolder { + std::function cancelAll; + QFuture future; +}; + +QFuture CreateFuture(); + +template inline QFuture PreventFutureDeadlock(QFuture future) +{ + /* + * QFutures deadlock if there are continuations on the same thread that + * need to wait for the previous continuation to finish, see + * https://github.com/qt/qtbase/commit/59e21a536f7f81625216dc7a621e7be59919da33 + * + * related bugs: + * https://bugreports.qt.io/browse/QTBUG-119406 + * https://bugreports.qt.io/browse/QTBUG-119103 + * https://bugreports.qt.io/browse/QTBUG-117918 + * https://bugreports.qt.io/browse/QTBUG-119579 + * https://bugreports.qt.io/browse/QTBUG-119810 + * @RytoEX's summary: + * QTBUG-119406 and QTBUG-119103 affect Qt 6.6.0 and are fixed in Qt 6.6.2 and 6.7.0+. + * QTBUG-119579 and QTBUG-119810 affect Qt 6.6.1 and are fixed in Qt 6.6.2 and 6.7.0+. + * QTBUG-117918 is the only strange one that seems to possibly affect all Qt 6.x versions + * until 6.6.2, but only in Debug builds. + * + * To fix this, move relevant QFutures to another thread before resuming + * on main thread for affected Qt versions + */ +#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)) && \ + (QT_VERSION < QT_VERSION_CHECK(6, 6, 2)) + if (future.isFinished()) { + return future; + } + + return future.then(QtFuture::Launch::Async, [](T val) { return val; }); +#else + return future; +#endif +} diff --git a/UI/system-info-macos.mm b/UI/system-info-macos.mm new file mode 100644 index 000000000..0c81e4cfb --- /dev/null +++ b/UI/system-info-macos.mm @@ -0,0 +1,6 @@ +#include "system-info.hpp" + +void system_info(GoLiveApi::Capabilities &capabilities) +{ + UNUSED_PARAMETER(capabilities); +} diff --git a/UI/system-info-posix.cpp b/UI/system-info-posix.cpp new file mode 100644 index 000000000..00affc68e --- /dev/null +++ b/UI/system-info-posix.cpp @@ -0,0 +1,6 @@ +#include "system-info.hpp" + +void system_info(GoLiveApi::Capabilities &capabilities) +{ + UNUSED_PARAMETER(capabilities); +} diff --git a/UI/system-info-windows.cpp b/UI/system-info-windows.cpp new file mode 100644 index 000000000..565e6d521 --- /dev/null +++ b/UI/system-info-windows.cpp @@ -0,0 +1,278 @@ +#include "system-info.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include + +static std::optional> system_gpu_data() +{ + ComPtr factory; + ComPtr adapter; + HRESULT hr; + UINT i; + + hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory)); + if (FAILED(hr)) + return std::nullopt; + + std::vector adapter_info; + + DStr luid_buffer; + for (i = 0; factory->EnumAdapters1(i, adapter.Assign()) == S_OK; ++i) { + DXGI_ADAPTER_DESC desc; + char name[512] = ""; + char driver_version[512] = ""; + + hr = adapter->GetDesc(&desc); + if (FAILED(hr)) + continue; + + /* ignore Microsoft's 'basic' renderer' */ + if (desc.VendorId == 0x1414 && desc.DeviceId == 0x8c) + continue; + + os_wcs_to_utf8(desc.Description, 0, name, sizeof(name)); + + GoLiveApi::Gpu data; + data.model = name; + + data.vendor_id = desc.VendorId; + data.device_id = desc.DeviceId; + + data.dedicated_video_memory = desc.DedicatedVideoMemory; + data.shared_system_memory = desc.SharedSystemMemory; + + dstr_printf(luid_buffer, "luid_0x%08X_0x%08X", + desc.AdapterLuid.HighPart, + desc.AdapterLuid.LowPart); + data.luid = luid_buffer->array; + + /* driver version */ + LARGE_INTEGER umd; + hr = adapter->CheckInterfaceSupport(__uuidof(IDXGIDevice), + &umd); + if (SUCCEEDED(hr)) { + const uint64_t version = umd.QuadPart; + const uint16_t aa = (version >> 48) & 0xffff; + const uint16_t bb = (version >> 32) & 0xffff; + const uint16_t ccccc = (version >> 16) & 0xffff; + const uint16_t ddddd = version & 0xffff; + snprintf(driver_version, sizeof(driver_version), + "%" PRIu16 ".%" PRIu16 ".%" PRIu16 ".%" PRIu16, + aa, bb, ccccc, ddddd); + data.driver_version = driver_version; + } + + adapter_info.push_back(data); + } + + return adapter_info; +} + +static void get_processor_info(char **name, DWORD *speed) +{ + HKEY key; + wchar_t data[1024]; + DWORD size; + LSTATUS status; + + memset(data, 0, sizeof(data)); + + status = RegOpenKeyW( + HKEY_LOCAL_MACHINE, + L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", &key); + if (status != ERROR_SUCCESS) + return; + + size = sizeof(data); + status = RegQueryValueExW(key, L"ProcessorNameString", NULL, NULL, + (LPBYTE)data, &size); + if (status == ERROR_SUCCESS) { + os_wcs_to_utf8_ptr(data, 0, name); + } else { + *name = 0; + } + + size = sizeof(*speed); + status = RegQueryValueExW(key, L"~MHz", NULL, NULL, (LPBYTE)speed, + &size); + if (status != ERROR_SUCCESS) + *speed = 0; + + RegCloseKey(key); +} + +#define WIN10_GAME_BAR_REG_KEY \ + L"Software\\Microsoft\\Windows\\CurrentVersion\\GameDVR" +#define WIN10_GAME_DVR_POLICY_REG_KEY \ + L"SOFTWARE\\Policies\\Microsoft\\Windows\\GameDVR" +#define WIN10_GAME_DVR_REG_KEY L"System\\GameConfigStore" +#define WIN10_GAME_MODE_REG_KEY L"Software\\Microsoft\\GameBar" +#define WIN10_HAGS_REG_KEY \ + L"SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers" + +static std::optional +get_gaming_features_data(const win_version_info &ver) +{ + uint32_t win_ver = (ver.major << 8) | ver.minor; + if (win_ver < 0xA00) + return std::nullopt; + + GoLiveApi::GamingFeatures gaming_features; + + struct feature_mapping_s { + std::optional *field; + HKEY hkey; + LPCWSTR sub_key; + LPCWSTR value_name; + LPCWSTR backup_value_name; + bool non_existence_is_false; + DWORD disabled_value; + }; + struct feature_mapping_s features[] = { + {&gaming_features.game_bar_enabled, HKEY_CURRENT_USER, + WIN10_GAME_BAR_REG_KEY, L"AppCaptureEnabled", 0, false, 0}, + {&gaming_features.game_dvr_allowed, HKEY_CURRENT_USER, + WIN10_GAME_DVR_POLICY_REG_KEY, L"AllowGameDVR", 0, false, 0}, + {&gaming_features.game_dvr_enabled, HKEY_CURRENT_USER, + WIN10_GAME_DVR_REG_KEY, L"GameDVR_Enabled", 0, false, 0}, + {&gaming_features.game_dvr_bg_recording, HKEY_CURRENT_USER, + WIN10_GAME_BAR_REG_KEY, L"HistoricalCaptureEnabled", 0, false, + 0}, + {&gaming_features.game_mode_enabled, HKEY_CURRENT_USER, + WIN10_GAME_MODE_REG_KEY, L"AutoGameModeEnabled", + L"AllowAutoGameMode", false, 0}, + {&gaming_features.hags_enabled, HKEY_LOCAL_MACHINE, + WIN10_HAGS_REG_KEY, L"HwSchMode", 0, true, 1}}; + + for (int i = 0; i < sizeof(features) / sizeof(*features); ++i) { + struct reg_dword info; + + get_reg_dword(features[i].hkey, features[i].sub_key, + features[i].value_name, &info); + + if (info.status != ERROR_SUCCESS && + features[i].backup_value_name) { + get_reg_dword(features[i].hkey, features[i].sub_key, + features[i].backup_value_name, &info); + } + + if (info.status == ERROR_SUCCESS) { + *features[i].field = info.return_value != + features[i].disabled_value; + } else if (features[i].non_existence_is_false) { + *features[i].field = false; + } + } + + return gaming_features; +} + +static inline bool get_reg_sz(HKEY key, const wchar_t *val, wchar_t *buf, + DWORD size) +{ + const LSTATUS status = + RegGetValueW(key, NULL, val, RRF_RT_REG_SZ, NULL, buf, &size); + return status == ERROR_SUCCESS; +} + +#define MAX_SZ_LEN 256 +#define WINVER_REG_KEY L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion" + +static char win_release_id[MAX_SZ_LEN] = "unavailable"; + +static inline void get_reg_ver(struct win_version_info *ver) +{ + HKEY key; + DWORD size, dw_val; + LSTATUS status; + wchar_t str[MAX_SZ_LEN]; + + status = RegOpenKeyW(HKEY_LOCAL_MACHINE, WINVER_REG_KEY, &key); + if (status != ERROR_SUCCESS) + return; + + size = sizeof(dw_val); + + status = RegQueryValueExW(key, L"CurrentMajorVersionNumber", NULL, NULL, + (LPBYTE)&dw_val, &size); + if (status == ERROR_SUCCESS) + ver->major = (int)dw_val; + + status = RegQueryValueExW(key, L"CurrentMinorVersionNumber", NULL, NULL, + (LPBYTE)&dw_val, &size); + if (status == ERROR_SUCCESS) + ver->minor = (int)dw_val; + + status = RegQueryValueExW(key, L"UBR", NULL, NULL, (LPBYTE)&dw_val, + &size); + if (status == ERROR_SUCCESS) + ver->revis = (int)dw_val; + + if (get_reg_sz(key, L"CurrentBuildNumber", str, sizeof(str))) { + ver->build = wcstol(str, NULL, 10); + } + + const wchar_t *release_key = ver->build > 19041 ? L"DisplayVersion" + : L"ReleaseId"; + if (get_reg_sz(key, release_key, str, sizeof(str))) { + os_wcs_to_utf8(str, 0, win_release_id, MAX_SZ_LEN); + } + + RegCloseKey(key); +} + +void system_info(GoLiveApi::Capabilities &capabilities) +{ + char tmpstr[1024]; + + capabilities.gpu = system_gpu_data(); + + { + auto &cpu_data = capabilities.cpu; + cpu_data.physical_cores = os_get_physical_cores(); + cpu_data.logical_cores = os_get_logical_cores(); + DWORD processorSpeed; + char *processorName; + get_processor_info(&processorName, &processorSpeed); + if (processorSpeed) + cpu_data.speed = processorSpeed; + if (processorName) + cpu_data.name = processorName; + bfree(processorName); + } + + { + auto &memory_data = capabilities.memory; + memory_data.total = os_get_sys_total_size(); + memory_data.free = os_get_sys_free_size(); + } + + struct win_version_info ver; + get_win_ver(&ver); + get_reg_ver(&ver); + + // Gaming features + capabilities.gaming_features = get_gaming_features_data(ver); + + { + auto &system_data = capabilities.system; + + snprintf(tmpstr, sizeof(tmpstr), "%d.%d", ver.major, ver.minor); + + system_data.version = tmpstr; + system_data.name = "Windows"; + system_data.build = ver.build; + system_data.release = win_release_id; + system_data.revision = ver.revis; + system_data.bits = is_64_bit_windows() ? 64 : 32; + system_data.arm = is_arm64_windows(); + system_data.armEmulation = os_get_emulation_status(); + } +} diff --git a/UI/system-info.hpp b/UI/system-info.hpp new file mode 100644 index 000000000..231b23ab2 --- /dev/null +++ b/UI/system-info.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include "models/multitrack-video.hpp" + +void system_info(GoLiveApi::Capabilities &capabilities); diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp index 7d7c4278e..904875783 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/UI/window-basic-auto-config-test.cpp @@ -280,6 +280,34 @@ void AutoConfigTestPage::TestBandwidthThread() servers.resize(2); } + if (!wiz->serviceConfigServers.empty()) { + if (wiz->service == AutoConfig::Service::Twitch && + wiz->twitchAuto) { + // servers from Twitch service config replace the "auto" entry + servers.erase(servers.begin()); + } + + for (auto it = std::rbegin(wiz->serviceConfigServers); + it != std::rend(wiz->serviceConfigServers); it++) { + auto same_server = std::find_if( + std::begin(servers), std::end(servers), + [&](const ServerInfo &si) { + return si.address == it->address; + }); + if (same_server != std::end(servers)) + servers.erase(same_server); + servers.emplace(std::begin(servers), it->name.c_str(), + it->address.c_str()); + } + + if (wiz->service == AutoConfig::Service::Twitch && + wiz->twitchAuto) { + // see above, only test 3 servers + // rtmps urls are currently counted as separate servers + servers.resize(3); + } + } + /* -----------------------------------*/ /* apply service settings */ @@ -287,6 +315,11 @@ void AutoConfigTestPage::TestBandwidthThread() obs_service_apply_encoder_settings(service, vencoder_settings, aencoder_settings); + if (wiz->multitrackVideo.testSuccessful) { + obs_data_set_int(vencoder_settings, "bitrate", + wiz->startingBitrate); + } + /* -----------------------------------*/ /* create output */ @@ -823,6 +856,10 @@ bool AutoConfigTestPage::TestSoftwareEncoding() upperBitrate /= 100; } + if (wiz->testMultitrackVideo && wiz->multitrackVideo.testSuccessful && + !wiz->multitrackVideo.bitrate.has_value()) + wiz->multitrackVideo.bitrate = wiz->idealBitrate; + if (wiz->idealBitrate > upperBitrate) wiz->idealBitrate = upperBitrate; @@ -1080,6 +1117,11 @@ void AutoConfigTestPage::FinalizeResults() OBSDataAutoRelease service_settings = obs_data_create(); OBSDataAutoRelease vencoder_settings = obs_data_create(); + if (wiz->testMultitrackVideo && + wiz->multitrackVideo.testSuccessful && + !wiz->multitrackVideo.bitrate.has_value()) + wiz->multitrackVideo.bitrate = wiz->idealBitrate; + obs_data_set_int(vencoder_settings, "bitrate", wiz->idealBitrate); @@ -1121,12 +1163,30 @@ void AutoConfigTestPage::FinalizeResults() form->addRow(newLabel("Basic.AutoConfig.StreamPage.Server"), new QLabel(wiz->serverName.c_str(), ui->finishPage)); - form->addRow(newLabel("Basic.Settings.Output.VideoBitrate"), - new QLabel(QString::number(wiz->idealBitrate), - ui->finishPage)); - form->addRow(newLabel(TEST_RESULT_SE), - new QLabel(encName(wiz->streamingEncoder), - ui->finishPage)); + form->addRow( + newLabel("Basic.Settings.Stream.MultitrackVideoLabel"), + newLabel(wiz->multitrackVideo.testSuccessful ? "Yes" + : "No")); + + if (wiz->multitrackVideo.testSuccessful) { + form->addRow( + newLabel("Basic.Settings.Output.VideoBitrate"), + newLabel("Automatic")); + form->addRow(newLabel(TEST_RESULT_SE), + newLabel("Automatic")); + form->addRow( + newLabel( + "Basic.AutoConfig.TestPage.Result.StreamingResolution"), + newLabel("Automatic")); + } else { + form->addRow( + newLabel("Basic.Settings.Output.VideoBitrate"), + new QLabel(QString::number(wiz->idealBitrate), + ui->finishPage)); + form->addRow(newLabel(TEST_RESULT_SE), + new QLabel(encName(wiz->streamingEncoder), + ui->finishPage)); + } } QString baseRes = @@ -1168,6 +1228,8 @@ void AutoConfigTestPage::FinalizeResults() new QLabel(scaleRes, ui->finishPage)); form->addRow(newLabel("Basic.Settings.Video.FPS"), new QLabel(fpsStr, ui->finishPage)); + + // FIXME: form layout is super squished, probably need to set proper sizepolicy on all widgets? } #define STARTING_SEPARATOR \ diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index f992c0719..da2ecb0cd 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -3,12 +3,18 @@ #include +#include + #include "window-basic-auto-config.hpp" #include "window-basic-main.hpp" #include "qt-wrappers.hpp" #include "obs-app.hpp" #include "url-push-button.hpp" +#include "goliveapi-postdata.hpp" +#include "goliveapi-network.hpp" +#include "multitrack-video-error.hpp" + #include "ui_AutoConfigStartPage.h" #include "ui_AutoConfigVideoPage.h" #include "ui_AutoConfigStreamPage.h" @@ -260,6 +266,7 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) ui->bitrate->setVisible(false); ui->connectAccount2->setVisible(false); ui->disconnectAccount->setVisible(false); + ui->useMultitrackVideo->setVisible(false); ui->connectedAccountLabel->setVisible(false); ui->connectedAccountText->setVisible(false); @@ -410,6 +417,76 @@ bool AutoConfigStreamPage::validatePage() wiz->service = AutoConfig::Service::Other; } + if (wiz->service == AutoConfig::Service::Twitch) { + wiz->testMultitrackVideo = ui->useMultitrackVideo->isChecked(); + + auto postData = + constructGoLivePost(QString::fromStdString(wiz->key), + std::nullopt, std::nullopt, false); + + OBSDataAutoRelease service_settings = + obs_service_get_settings(service); + auto multitrack_video_name = + QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(service_settings, + "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string( + service_settings, "multitrack_video_name"); + } + + try { + auto config = DownloadGoLiveConfig( + this, MultitrackVideoAutoConfigURL(service), + postData, multitrack_video_name); + + for (const auto &endpoint : config.ingest_endpoints) { + if (qstrnicmp("RTMP", endpoint.protocol.c_str(), + 4) != 0) + continue; + + std::string address = endpoint.url_template; + auto pos = address.find("/{stream_key}"); + if (pos != address.npos) + address.erase(pos); + + wiz->serviceConfigServers.push_back( + {address, address}); + } + + int multitrackVideoBitrate = 0; + for (auto &encoder_config : + config.encoder_configurations) { + multitrackVideoBitrate += + encoder_config.config.bitrate; + } + + // grab a streamkey from the go live config if we can + for (auto &endpoint : config.ingest_endpoints) { + const char *p = endpoint.protocol.c_str(); + const char *auth = + endpoint.authentication + ? endpoint.authentication + ->c_str() + : nullptr; + if (qstrnicmp("RTMP", p, 4) == 0 && auth && + *auth) { + wiz->key = auth; + break; + } + } + + if (multitrackVideoBitrate > 0) { + wiz->startingBitrate = multitrackVideoBitrate; + wiz->idealBitrate = multitrackVideoBitrate; + wiz->multitrackVideo.targetBitrate = + multitrackVideoBitrate; + wiz->multitrackVideo.testSuccessful = true; + } + } catch (const MultitrackVideoError & /*err*/) { + // FIXME: do something sensible + } + } + if (wiz->service != AutoConfig::Service::Twitch && wiz->service != AutoConfig::Service::YouTube && wiz->bandwidthTest) { @@ -561,6 +638,22 @@ void AutoConfigStreamPage::on_useStreamKey_clicked() UpdateCompleted(); } +void AutoConfigStreamPage::on_preferHardware_clicked() +{ + auto *main = OBSBasic::Get(); + bool multitrackVideoEnabled = + config_has_user_value(main->Config(), "Stream1", + "EnableMultitrackVideo") + ? config_get_bool(main->Config(), "Stream1", + "EnableMultitrackVideo") + : true; + + ui->useMultitrackVideo->setEnabled(ui->preferHardware->isChecked()); + ui->multitrackVideoInfo->setEnabled(ui->preferHardware->isChecked()); + ui->useMultitrackVideo->setChecked(ui->preferHardware->isChecked() && + multitrackVideoEnabled); +} + static inline bool is_auth_service(const std::string &service) { return Auth::AuthType(service) != Auth::Type::None; @@ -627,6 +720,48 @@ void AutoConfigStreamPage::ServiceChanged() bool testBandwidth = ui->doBandwidthTest->isChecked(); bool custom = IsCustomService(); + bool ertmp_multitrack_video_available = service == "Twitch"; + + bool custom_disclaimer = false; + auto multitrack_video_name = + QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (!custom) { + OBSDataAutoRelease service_settings = obs_data_create(); + obs_data_set_string(service_settings, "service", + service.c_str()); + OBSServiceAutoRelease obs_service = + obs_service_create("rtmp_common", "temp service", + service_settings, nullptr); + + if (obs_data_has_user_value(service_settings, + "multitrack_video_name")) { + multitrack_video_name = obs_data_get_string( + service_settings, "multitrack_video_name"); + } + + if (obs_data_has_user_value(service_settings, + "multitrack_video_disclaimer")) { + ui->multitrackVideoInfo->setText(obs_data_get_string( + service_settings, + "multitrack_video_disclaimer")); + custom_disclaimer = true; + } + } + + if (!custom_disclaimer) { + ui->multitrackVideoInfo->setText( + QTStr("MultitrackVideo.Info") + .arg(multitrack_video_name, service.c_str())); + } + + ui->multitrackVideoInfo->setVisible(ertmp_multitrack_video_available); + ui->useMultitrackVideo->setVisible(ertmp_multitrack_video_available); + ui->useMultitrackVideo->setText( + QTStr("Basic.AutoConfig.StreamPage.UseMultitrackVideo") + .arg(multitrack_video_name)); + ui->multitrackVideoInfo->setEnabled(wiz->hardwareEncodingAvailable); + ui->useMultitrackVideo->setEnabled(wiz->hardwareEncodingAvailable); + reset_service_ui_fields(service); /* Test three closest servers if "Auto" is available for Twitch */ @@ -977,12 +1112,21 @@ AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) if (!key.empty()) streamPage->ui->key->setText(key.c_str()); + TestHardwareEncoding(); + int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); + bool multitrackVideoEnabled = + config_has_user_value(main->Config(), "Stream1", + "EnableMultitrackVideo") + ? config_get_bool(main->Config(), "Stream1", + "EnableMultitrackVideo") + : true; streamPage->ui->bitrate->setValue(bitrate); + streamPage->ui->useMultitrackVideo->setChecked( + hardwareEncodingAvailable && multitrackVideoEnabled); streamPage->ServiceChanged(); - TestHardwareEncoding(); if (!hardwareEncodingAvailable) { delete streamPage->ui->preferHardware; streamPage->ui->preferHardware = nullptr; @@ -1142,6 +1286,33 @@ void AutoConfig::SaveStreamSettings() config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder)); config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced"); + + config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", + multitrackVideo.testSuccessful); + + if (multitrackVideo.targetBitrate.has_value()) + config_set_int(main->Config(), "Stream1", + "MultitrackVideoTargetBitrate", + *multitrackVideo.targetBitrate); + else + config_remove_value(main->Config(), "Stream1", + "MultitrackVideoTargetBitrate"); + + if (multitrackVideo.bitrate.has_value() && + multitrackVideo.targetBitrate.has_value() && + (static_cast(*multitrackVideo.bitrate) / + *multitrackVideo.targetBitrate) >= 0.90) { + config_set_bool(main->Config(), "Stream1", + "MultitrackVideoMaximumAggregateBitrateAuto", + true); + } else if (multitrackVideo.bitrate.has_value()) { + config_set_bool(main->Config(), "Stream1", + "MultitrackVideoMaximumAggregateBitrateAuto", + false); + config_set_int(main->Config(), "Stream1", + "MultitrackVideoMaximumAggregateBitrate", + *multitrackVideo.bitrate); + } } void AutoConfig::SaveSettings() diff --git a/UI/window-basic-auto-config.hpp b/UI/window-basic-auto-config.hpp index 5d966c795..2f93c540b 100644 --- a/UI/window-basic-auto-config.hpp +++ b/UI/window-basic-auto-config.hpp @@ -12,6 +12,7 @@ #include #include #include +#include class Ui_AutoConfigStartPage; class Ui_AutoConfigVideoPage; @@ -64,6 +65,11 @@ class AutoConfig : public QWizard { fps60, }; + struct StreamServer { + std::string name; + std::string address; + }; + static inline const char *GetEncoderId(Encoder enc); AutoConfigStreamPage *streamPage = nullptr; @@ -75,6 +81,11 @@ class AutoConfig : public QWizard { Type type = Type::Streaming; FPSType fpsType = FPSType::PreferHighFPS; int idealBitrate = 2500; + struct { + std::optional targetBitrate; + std::optional bitrate; + bool testSuccessful = false; + } multitrackVideo; int baseResolutionCX = 1920; int baseResolutionCY = 1080; int idealResolutionCX = 1280; @@ -84,6 +95,7 @@ class AutoConfig : public QWizard { std::string serviceName; std::string serverName; std::string server; + std::vector serviceConfigServers; std::string key; bool hardwareEncodingAvailable = false; @@ -95,6 +107,7 @@ class AutoConfig : public QWizard { int startingBitrate = 2500; bool customServer = false; bool bandwidthTest = false; + bool testMultitrackVideo = false; bool testRegions = true; bool twitchAuto = false; bool regionUS = true; @@ -195,6 +208,7 @@ public slots: void on_connectAccount_clicked(); void on_disconnectAccount_clicked(); void on_useStreamKey_clicked(); + void on_preferHardware_clicked(); void ServiceChanged(); void UpdateKeyLink(); void UpdateMoreInfoLink(); diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index 21548c6e2..b9a7e3d8a 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -1,8 +1,11 @@ #include #include +#include #include +#include #include "qt-wrappers.hpp" #include "audio-encoders.hpp" +#include "multitrack-video-error.hpp" #include "window-basic-main.hpp" #include "window-basic-main-outputs.hpp" #include "window-basic-vcam.hpp" @@ -67,6 +70,7 @@ static void OBSStopStreaming(void *data, calldata_t *params) output->streamingActive = false; output->delayActive = false; + output->multitrackVideoActive = false; os_atomic_set_bool(&streaming_active, false); QMetaObject::invokeMethod(output->main, "StreamingStop", Q_ARG(int, code), @@ -300,6 +304,18 @@ inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) deactivateVirtualCam.Connect(signal, "deactivate", OBSDeactivateVirtualCam, this); } + + auto multitrack_enabled = config_get_bool(main->Config(), "Stream1", + "EnableMultitrackVideo"); + if (!config_has_user_value(main->Config(), "Stream1", + "EnableMultitrackVideo")) { + auto service = main_->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + multitrack_enabled = obs_data_has_user_value( + settings, "multitrack_video_configuration_url"); + } + if (multitrack_enabled) + multitrackVideo = make_unique(); } extern void log_vcam_changed(const VCamConfig &config, bool starting); @@ -501,9 +517,11 @@ struct SimpleOutput : BasicOutputHandler { void UpdateRecording(); bool ConfigureRecording(bool useReplayBuffer); + bool IsVodTrackEnabled(obs_service_t *service); void SetupVodTrack(obs_service_t *service); - virtual bool SetupStreaming(obs_service_t *service) override; + virtual FutureHolder + SetupStreaming(obs_service_t *service) override; virtual bool StartStreaming(obs_service_t *service) override; virtual bool StartRecording() override; virtual bool StartReplayBuffer() override; @@ -1100,7 +1118,7 @@ const char *FindAudioEncoderFromCodec(const char *type) return nullptr; } -bool SimpleOutput::SetupStreaming(obs_service_t *service) +FutureHolder SimpleOutput::SetupStreaming(obs_service_t *service) { if (!Active()) SetupOutputs(); @@ -1113,46 +1131,72 @@ bool SimpleOutput::SetupStreaming(obs_service_t *service) const char *type = GetStreamOutputType(service); if (!type) - return false; + return {[] {}, CreateFuture().then([] { return false; })}; - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); + auto audio_bitrate = GetAudioBitrate(); + auto vod_track_mixer = IsVodTrackEnabled(service) ? std::optional{1} + : std::nullopt; - streamOutput = obs_output_create(type, "simple_stream", nullptr, - nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } + auto holder = SetupMultitrackVideo( + service, GetSimpleAACEncoderForBitrate(audio_bitrate), + vod_track_mixer); + auto future = + PreventFutureDeadlock(holder.future) + .then(main, [&](std::optional + multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); - streamDelayStarting.Connect( - obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.Connect( - obs_output_get_signal_handler(streamOutput), "stopping", - OBSStreamStopping, this); + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); - startStreaming.Connect( - obs_output_get_signal_handler(streamOutput), "start", - OBSStartStreaming, this); - stopStreaming.Connect( - obs_output_get_signal_handler(streamOutput), "stop", - OBSStopStreaming, this); + streamOutput = obs_output_create( + type, "simple_stream", nullptr, + nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } - outputType = type; - } + streamDelayStarting.Connect( + obs_output_get_signal_handler( + streamOutput), + "starting", OBSStreamStarting, + this); + streamStopping.Connect( + obs_output_get_signal_handler( + streamOutput), + "stopping", OBSStreamStopping, + this); - obs_output_set_video_encoder(streamOutput, videoStreaming); - obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); - obs_output_set_service(streamOutput, service); - return true; + startStreaming.Connect( + obs_output_get_signal_handler( + streamOutput), + "start", OBSStartStreaming, + this); + stopStreaming.Connect( + obs_output_get_signal_handler( + streamOutput), + "stop", OBSStopStreaming, this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, + videoStreaming); + obs_output_set_audio_encoder(streamOutput, + audioStreaming, 0); + obs_output_set_service(streamOutput, service); + return true; + }); + return {holder.cancelAll, future}; } static inline bool ServiceSupportsVodTrack(const char *service); @@ -1174,7 +1218,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) +bool SimpleOutput::IsVodTrackEnabled(obs_service_t *service) { bool advanced = config_get_bool(main->Config(), "SimpleOutput", "UseAdvanced"); @@ -1188,11 +1232,14 @@ void SimpleOutput::SetupVodTrack(obs_service_t *service) const char *id = obs_service_get_id(service); if (strcmp(id, "rtmp_custom") == 0) - enable = enableForCustomServer ? enable : false; + return enableForCustomServer ? enable : false; else - enable = advanced && enable && ServiceSupportsVodTrack(name); + return advanced && enable && ServiceSupportsVodTrack(name); +} - if (enable) +void SimpleOutput::SetupVodTrack(obs_service_t *service) +{ + if (IsVodTrackEnabled(service)) obs_output_set_audio_encoder(streamOutput, audioArchive, 1); else clear_archive_encoder(streamOutput, SIMPLE_ARCHIVE_NAME); @@ -1219,10 +1266,20 @@ bool SimpleOutput::StartStreaming(obs_service_t *service) "NewSocketLoopEnable"); bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; #endif bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings( + main, main->Config(), service, useDelay, + enableNewSocketLoop, enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + OBSDataAutoRelease settings = obs_data_create(); obs_data_set_string(settings, "bind_ip", bindIP); obs_data_set_string(settings, "ip_family", ipFamily); @@ -1233,6 +1290,10 @@ bool SimpleOutput::StartStreaming(obs_service_t *service) enableLowLatencyMode); #endif obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = + StreamingOutput(); // shadowing is sort of bad, but also convenient + obs_output_update(streamOutput, settings); if (!reconnect) @@ -1243,12 +1304,18 @@ bool SimpleOutput::StartStreaming(obs_service_t *service) obs_output_set_reconnect_settings(streamOutput, maxRetries, retryDelay); - SetupVodTrack(service); + if (!multitrackVideo || !multitrackVideoActive) + SetupVodTrack(service); if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); return true; } + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + const char *error = obs_output_get_last_error(streamOutput); bool hasLastError = error && *error; if (hasLastError) @@ -1437,10 +1504,13 @@ bool SimpleOutput::StartReplayBuffer() void SimpleOutput::StopStreaming(bool force) { - if (force) - obs_output_force_stop(streamOutput); + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); else - obs_output_stop(streamOutput); + obs_output_stop(output); } void SimpleOutput::StopRecording(bool force) @@ -1461,7 +1531,7 @@ void SimpleOutput::StopReplayBuffer(bool force) bool SimpleOutput::StreamingActive() const { - return obs_output_active(streamOutput); + return obs_output_active(StreamingOutput()); } bool SimpleOutput::RecordingActive() const @@ -1497,6 +1567,7 @@ struct AdvancedOutput : BasicOutputHandler { inline void UpdateAudioSettings(); virtual void Update() override; + inline std::optional VodTrackMixerIdx(obs_service_t *service); inline void SetupVodTrack(obs_service_t *service); inline void SetupStreaming(); @@ -1505,7 +1576,8 @@ struct AdvancedOutput : BasicOutputHandler { void SetupOutputs() override; int GetAudioBitrate(size_t i, const char *id) const; - virtual bool SetupStreaming(obs_service_t *service) override; + virtual FutureHolder + SetupStreaming(obs_service_t *service) override; virtual bool StartStreaming(obs_service_t *service) override; virtual bool StartRecording() override; virtual bool StartReplayBuffer() override; @@ -2148,7 +2220,8 @@ int AdvancedOutput::GetAudioBitrate(size_t i, const char *id) const return FindClosestAvailableAudioBitrate(id, bitrate); } -inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +inline std::optional +AdvancedOutput::VodTrackMixerIdx(obs_service_t *service) { int streamTrackIndex = config_get_int(main->Config(), "AdvOut", "TrackIndex"); @@ -2169,13 +2242,21 @@ inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) if (!ServiceSupportsVodTrack(service)) vodTrackEnabled = false; } + if (vodTrackEnabled && streamTrackIndex != vodTrackIndex) + return {vodTrackIndex - 1}; + return std::nullopt; +} + +inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) +{ + if (VodTrackMixerIdx(service).has_value()) obs_output_set_audio_encoder(streamOutput, streamArchiveEnc, 1); else clear_archive_encoder(streamOutput, ADV_ARCHIVE_NAME); } -bool AdvancedOutput::SetupStreaming(obs_service_t *service) +FutureHolder AdvancedOutput::SetupStreaming(obs_service_t *service) { int multiTrackAudioMixes = config_get_int(main->Config(), "AdvOut", "StreamMultiTrackAudioMixes"); @@ -2201,55 +2282,88 @@ bool AdvancedOutput::SetupStreaming(obs_service_t *service) const char *type = GetStreamOutputType(service); if (!type) - return false; + return {[] {}, CreateFuture().then(main, [] { return false; })}; - /* XXX: this is messy and disgusting and should be refactored */ - if (outputType != type) { - streamDelayStarting.Disconnect(); - streamStopping.Disconnect(); - startStreaming.Disconnect(); - stopStreaming.Disconnect(); + const char *audio_encoder_id = + config_get_string(main->Config(), "AdvOut", "AudioEncoder"); - streamOutput = - obs_output_create(type, "adv_stream", nullptr, nullptr); - if (!streamOutput) { - blog(LOG_WARNING, - "Creation of stream output type '%s' " - "failed!", - type); - return false; - } + auto holder = SetupMultitrackVideo(service, audio_encoder_id, + VodTrackMixerIdx(service)); + auto future = + PreventFutureDeadlock(holder.future) + .then(main, [&](std::optional + multitrackVideoResult) { + if (multitrackVideoResult.has_value()) + return multitrackVideoResult.value(); - streamDelayStarting.Connect( - obs_output_get_signal_handler(streamOutput), "starting", - OBSStreamStarting, this); - streamStopping.Connect( - obs_output_get_signal_handler(streamOutput), "stopping", - OBSStreamStopping, this); + /* XXX: this is messy and disgusting and should be refactored */ + if (outputType != type) { + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); - startStreaming.Connect( - obs_output_get_signal_handler(streamOutput), "start", - OBSStartStreaming, this); - stopStreaming.Connect( - obs_output_get_signal_handler(streamOutput), "stop", - OBSStopStreaming, this); + streamOutput = obs_output_create( + type, "adv_stream", nullptr, + nullptr); + if (!streamOutput) { + blog(LOG_WARNING, + "Creation of stream output type '%s' " + "failed!", + type); + return false; + } - outputType = type; - } + streamDelayStarting.Connect( + obs_output_get_signal_handler( + streamOutput), + "starting", OBSStreamStarting, + this); + streamStopping.Connect( + obs_output_get_signal_handler( + streamOutput), + "stopping", OBSStreamStopping, + this); - obs_output_set_video_encoder(streamOutput, videoStreaming); - if (!is_multitrack_output) { - obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); - } else { - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if ((multiTrackAudioMixes & (1 << i)) != 0) { - obs_output_set_audio_encoder( - streamOutput, streamTrack[i], idx); - idx++; - } - } - } - return true; + startStreaming.Connect( + obs_output_get_signal_handler( + streamOutput), + "start", OBSStartStreaming, + this); + stopStreaming.Connect( + obs_output_get_signal_handler( + streamOutput), + "stop", OBSStopStreaming, this); + + outputType = type; + } + + obs_output_set_video_encoder(streamOutput, + videoStreaming); + obs_output_set_audio_encoder(streamOutput, + streamAudioEnc, 0); + + if (!is_multitrack_output) { + obs_output_set_audio_encoder( + streamOutput, streamAudioEnc, + 0); + } else { + for (int i = 0; i < MAX_AUDIO_MIXES; + i++) { + if ((multiTrackAudioMixes & + (1 << i)) != 0) { + obs_output_set_audio_encoder( + streamOutput, + streamTrack[i], + idx); + idx++; + } + } + } + + return true; + }); + return {holder.cancelAll, future}; } bool AdvancedOutput::StartStreaming(obs_service_t *service) @@ -2273,10 +2387,20 @@ bool AdvancedOutput::StartStreaming(obs_service_t *service) "NewSocketLoopEnable"); bool enableLowLatencyMode = config_get_bool(main->Config(), "Output", "LowLatencyEnable"); +#else + bool enableNewSocketLoop = false; #endif bool enableDynBitrate = config_get_bool(main->Config(), "Output", "DynamicBitrate"); + if (multitrackVideo && multitrackVideoActive && + !multitrackVideo->HandleIncompatibleSettings( + main, main->Config(), service, useDelay, + enableNewSocketLoop, enableDynBitrate)) { + multitrackVideoActive = false; + return false; + } + bool is_rtmp = false; obs_service_t *service_obj = main->GetService(); const char *protocol = obs_service_get_protocol(service_obj); @@ -2296,6 +2420,10 @@ bool AdvancedOutput::StartStreaming(obs_service_t *service) enableLowLatencyMode); #endif obs_data_set_bool(settings, "dyn_bitrate", enableDynBitrate); + + auto streamOutput = + StreamingOutput(); // shadowing is sort of bad, but also convenient + obs_output_update(streamOutput, settings); if (!reconnect) @@ -2309,9 +2437,14 @@ bool AdvancedOutput::StartStreaming(obs_service_t *service) SetupVodTrack(service); } if (obs_output_start(streamOutput)) { + if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StartedStreaming(); return true; } + if (multitrackVideo && multitrackVideoActive) + multitrackVideoActive = false; + const char *error = obs_output_get_last_error(streamOutput); bool hasLastError = error && *error; if (hasLastError) @@ -2341,7 +2474,7 @@ bool AdvancedOutput::StartRecording() if (!ffmpegOutput) { UpdateRecordingSettings(); } - } else if (!obs_output_active(streamOutput)) { + } else if (!obs_output_active(StreamingOutput())) { UpdateStreamSettings(); } @@ -2440,7 +2573,7 @@ bool AdvancedOutput::StartReplayBuffer() if (!useStreamEncoder) { if (!ffmpegOutput) UpdateRecordingSettings(); - } else if (!obs_output_active(streamOutput)) { + } else if (!obs_output_active(StreamingOutput())) { UpdateStreamSettings(); } @@ -2504,10 +2637,13 @@ bool AdvancedOutput::StartReplayBuffer() void AdvancedOutput::StopStreaming(bool force) { - if (force) - obs_output_force_stop(streamOutput); + auto output = StreamingOutput(); + if (force && output) + obs_output_force_stop(output); + else if (multitrackVideo && multitrackVideoActive) + multitrackVideo->StopStreaming(); else - obs_output_stop(streamOutput); + obs_output_stop(output); } void AdvancedOutput::StopRecording(bool force) @@ -2528,7 +2664,7 @@ void AdvancedOutput::StopReplayBuffer(bool force) bool AdvancedOutput::StreamingActive() const { - return obs_output_active(streamOutput); + return obs_output_active(StreamingOutput()); } bool AdvancedOutput::RecordingActive() const @@ -2563,6 +2699,174 @@ std::string BasicOutputHandler::GetRecordingFilename( return dst; } +extern std::string DeserializeConfigText(const char *text); + +FutureHolder> +BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service, + std::string audio_encoder_id, + std::optional vod_track_mixer) +{ + if (!multitrackVideo) + return {[] {}, CreateFuture().then([] { + return std::optional{std::nullopt}; + })}; + + multitrackVideoActive = false; + + streamDelayStarting.Disconnect(); + streamStopping.Disconnect(); + startStreaming.Disconnect(); + stopStreaming.Disconnect(); + + bool is_custom = + strncmp("rtmp_custom", obs_service_get_type(service), 11) == 0; + + std::optional custom_config = std::nullopt; + if (config_get_bool(main->Config(), "Stream1", + "MultitrackVideoConfigOverrideEnabled")) + custom_config = DeserializeConfigText( + config_get_string(main->Config(), "Stream1", + "MultitrackVideoConfigOverride")); + + OBSDataAutoRelease settings = obs_service_get_settings(service); + QString key = obs_data_get_string(settings, "key"); + + const char *service_name = ""; + if (is_custom && obs_data_has_user_value(settings, "service_name")) { + service_name = obs_data_get_string(settings, "service_name"); + } else if (!is_custom) { + service_name = obs_data_get_string(settings, "service"); + } + + std::optional custom_rtmp_url; + auto server = obs_data_get_string(settings, "server"); + if (strcmp(server, "auto") != 0) { + custom_rtmp_url = server; + } + + auto service_custom_server = + obs_data_get_bool(settings, "using_custom_server"); + if (custom_rtmp_url.has_value()) { + blog(LOG_INFO, "Using %sserver '%s'", + service_custom_server ? "custom " : "", + custom_rtmp_url->c_str()); + } + + auto maximum_aggregate_bitrate = + config_get_bool(main->Config(), "Stream1", + "MultitrackVideoMaximumAggregateBitrateAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", + "MultitrackVideoMaximumAggregateBitrate")); + + auto maximum_video_tracks = + config_get_bool(main->Config(), "Stream1", + "MultitrackVideoMaximumVideoTracksAuto") + ? std::nullopt + : std::make_optional(config_get_int( + main->Config(), "Stream1", + "MultitrackVideoMaximumVideoTracks")); + + auto stream_dump_config = GenerateMultitrackVideoStreamDumpConfig(); + + auto firstFuture = CreateFuture().then( + QThreadPool::globalInstance(), + [=, multitrackVideo = multitrackVideo.get(), + service_name = std::string{service_name}, + service = OBSService{service}, + stream_dump_config = std::move(stream_dump_config)]() + -> std::optional { + try { + multitrackVideo->PrepareStreaming( + main, service_name.c_str(), service, + custom_rtmp_url, key, + audio_encoder_id.c_str(), + maximum_aggregate_bitrate, + maximum_video_tracks, custom_config, + stream_dump_config, vod_track_mixer); + } catch (const MultitrackVideoError &error) { + return error; + } + return std::nullopt; + }); + + auto secondFuture = firstFuture.then( + main, + [&, service = OBSService{service}]( + std::optional error) + -> std::optional { + if (error) { + OBSDataAutoRelease service_settings = + obs_service_get_settings(service); + auto multitrack_video_name = QTStr( + "Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value( + service_settings, + "multitrack_video_name")) { + multitrack_video_name = + obs_data_get_string( + service_settings, + "multitrack_video_name"); + } + + multitrackVideoActive = false; + if (!error->ShowDialog(main, + multitrack_video_name)) + return false; + return std::nullopt; + } + + multitrackVideoActive = true; + + auto signal_handler = + multitrackVideo->StreamingSignalHandler(); + + streamDelayStarting.Connect(signal_handler, "starting", + OBSStreamStarting, this); + streamStopping.Connect(signal_handler, "stopping", + OBSStreamStopping, this); + + startStreaming.Connect(signal_handler, "start", + OBSStartStreaming, this); + stopStreaming.Connect(signal_handler, "stop", + OBSStopStreaming, this); + return true; + }); + + return {[=]() mutable { firstFuture.cancel(); }, + PreventFutureDeadlock(secondFuture)}; +} + +OBSDataAutoRelease BasicOutputHandler::GenerateMultitrackVideoStreamDumpConfig() +{ + auto stream_dump_enabled = config_get_bool( + main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled"); + + if (!stream_dump_enabled) + return nullptr; + + const char *path = + config_get_string(main->Config(), "SimpleOutput", "FilePath"); + bool noSpace = config_get_bool(main->Config(), "SimpleOutput", + "FileNameWithoutSpace"); + const char *filenameFormat = config_get_string(main->Config(), "Output", + "FilenameFormatting"); + bool overwriteIfExists = + config_get_bool(main->Config(), "Output", "OverwriteIfExists"); + + string f; + + OBSDataAutoRelease settings = obs_data_create(); + f = GetFormatString(filenameFormat, nullptr, nullptr); + string strPath = GetRecordingFilename(path, "flv", noSpace, + overwriteIfExists, f.c_str(), + // never remux stream dump + false); + obs_data_set_string(settings, "path", strPath.c_str()); + return settings; +} + BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main) { return new SimpleOutput(main); diff --git a/UI/window-basic-main-outputs.hpp b/UI/window-basic-main-outputs.hpp index 0850f00a9..5dd86df46 100644 --- a/UI/window-basic-main-outputs.hpp +++ b/UI/window-basic-main-outputs.hpp @@ -1,7 +1,13 @@ #pragma once +#include #include +#include + +#include "qt-helpers.hpp" +#include "multitrack-video-output.hpp" + class OBSBasic; struct BasicOutputHandler { @@ -16,6 +22,17 @@ struct BasicOutputHandler { bool virtualCamActive = false; OBSBasic *main; + std::unique_ptr multitrackVideo; + bool multitrackVideoActive = false; + + OBSOutputAutoRelease StreamingOutput() const + { + return (multitrackVideo && multitrackVideoActive) + ? multitrackVideo->StreamingOutput() + : OBSOutputAutoRelease{ + obs_output_get_ref(streamOutput)}; + } + obs_view_t *virtualCamView = nullptr; video_t *virtualCamVideo = nullptr; obs_scene_t *vCamSourceScene = nullptr; @@ -46,7 +63,7 @@ struct BasicOutputHandler { virtual ~BasicOutputHandler(){}; - virtual bool SetupStreaming(obs_service_t *service) = 0; + virtual FutureHolder SetupStreaming(obs_service_t *service) = 0; virtual bool StartStreaming(obs_service_t *service) = 0; virtual bool StartRecording() = 0; virtual bool StartReplayBuffer() { return false; } @@ -70,7 +87,8 @@ struct BasicOutputHandler { inline bool Active() const { return streamingActive || recordingActive || delayActive || - replayBufferActive || virtualCamActive; + replayBufferActive || virtualCamActive || + multitrackVideoActive; } protected: @@ -79,6 +97,12 @@ protected: const char *container, bool noSpace, bool overwrite, const char *format, bool ffmpeg); + + FutureHolder> + SetupMultitrackVideo(obs_service_t *service, + std::string audio_encoder_id, + std::optional vod_track_mixer); + OBSDataAutoRelease GenerateMultitrackVideoStreamDumpConfig(); }; BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main); diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index f173886e1..ce74a8b65 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1615,6 +1615,13 @@ bool OBSBasic::InitBasicConfigDefaults() config_set_default_bool(basicConfig, "Stream1", "IgnoreRecommended", false); + config_set_default_bool(basicConfig, "Stream1", "EnableMultitrackVideo", + true); + config_set_default_bool(basicConfig, "Stream1", + "MultitrackVideoMaximumAggregateBitrateAuto", + true); + config_set_default_bool(basicConfig, "Stream1", + "MultitrackVideoMaximumVideoTracksAuto", true); config_set_default_string(basicConfig, "SimpleOutput", "FilePath", GetDefaultVideoSavePath().c_str()); @@ -1953,7 +1960,8 @@ void OBSBasic::ResetOutputs() const char *mode = config_get_string(basicConfig, "Output", "Mode"); bool advOut = astrcmpi(mode, "Advanced") == 0; - if (!outputHandler || !outputHandler->Active()) { + if ((!outputHandler || !outputHandler->Active()) && + startStreamingFuture.future.isFinished()) { outputHandler.reset(); outputHandler.reset(advOut ? CreateAdvancedOutputHandler(this) : CreateSimpleOutputHandler(this)); @@ -5097,6 +5105,22 @@ void OBSBasic::ClearSceneData() void OBSBasic::closeEvent(QCloseEvent *event) { + if (!startStreamingFuture.future.isFinished() && + !startStreamingFuture.future.isCanceled()) { + startStreamingFuture.future.onCanceled( + this, [basic = QPointer{this}] { + if (basic) + basic->close(); + }); + startStreamingFuture.cancelAll(); + event->ignore(); + return; + } else if (startStreamingFuture.future.isCanceled() && + !startStreamingFuture.future.isFinished()) { + event->ignore(); + return; + } + /* Do not close window if inside of a temporary event loop because we * could be inside of an Auth::LoadUI call. Keep trying once per * second until we've exit any known sub-loops. */ @@ -7016,68 +7040,88 @@ void OBSBasic::StartStreaming() } } - if (!outputHandler->SetupStreaming(service)) { - DisplayStreamStartError(); - return; - } - - if (api) - api->on_event(OBS_FRONTEND_EVENT_STREAMING_STARTING); - - SaveProject(); + auto setStreamText = [&](const QString &text) { + ui->streamButton->setText(text); + if (sysTrayStream) + sysTrayStream->setText(text); + }; ui->streamButton->setEnabled(false); ui->streamButton->setChecked(false); - ui->streamButton->setText(QTStr("Basic.Main.Connecting")); ui->broadcastButton->setChecked(false); - - if (sysTrayStream) { + if (sysTrayStream) sysTrayStream->setEnabled(false); - sysTrayStream->setText(ui->streamButton->text()); - } - if (!outputHandler->StartStreaming(service)) { - DisplayStreamStartError(); - return; - } + setStreamText(QTStr("Basic.Main.PreparingStream")); - if (!autoStartBroadcast) { - ui->broadcastButton->setText( - QTStr("Basic.Main.StartBroadcast")); - ui->broadcastButton->setProperty("broadcastState", "ready"); - ui->broadcastButton->style()->unpolish(ui->broadcastButton); - ui->broadcastButton->style()->polish(ui->broadcastButton); - // well, we need to disable button while stream is not active - ui->broadcastButton->setEnabled(false); - } else { - if (!autoStopBroadcast) { - ui->broadcastButton->setText( - QTStr("Basic.Main.StopBroadcast")); - } else { - ui->broadcastButton->setText( - QTStr("Basic.Main.AutoStopEnabled")); - ui->broadcastButton->setEnabled(false); - } - ui->broadcastButton->setProperty("broadcastState", "active"); - ui->broadcastButton->style()->unpolish(ui->broadcastButton); - ui->broadcastButton->style()->polish(ui->broadcastButton); - broadcastActive = true; - } + auto holder = outputHandler->SetupStreaming(service); + auto future = holder.future.then( + this, [&, setStreamText](bool setupStreamingResult) { + if (!setupStreamingResult) { + DisplayStreamStartError(); + return; + } - bool recordWhenStreaming = config_get_bool( - GetGlobalConfig(), "BasicWindow", "RecordWhenStreaming"); - if (recordWhenStreaming) - StartRecording(); + if (api) + api->on_event( + OBS_FRONTEND_EVENT_STREAMING_STARTING); - bool replayBufferWhileStreaming = config_get_bool( - GetGlobalConfig(), "BasicWindow", "ReplayBufferWhileStreaming"); - if (replayBufferWhileStreaming) - StartReplayBuffer(); + SaveProject(); + + setStreamText(QTStr("Basic.Main.Connecting")); + + if (!outputHandler->StartStreaming(service)) { + DisplayStreamStartError(); + return; + } + + if (!autoStartBroadcast) { + ui->broadcastButton->setText( + QTStr("Basic.Main.StartBroadcast")); + ui->broadcastButton->setProperty( + "broadcastState", "ready"); + ui->broadcastButton->style()->unpolish( + ui->broadcastButton); + ui->broadcastButton->style()->polish( + ui->broadcastButton); + // well, we need to disable button while stream is not active + ui->broadcastButton->setEnabled(false); + } else { + if (!autoStopBroadcast) { + ui->broadcastButton->setText(QTStr( + "Basic.Main.StopBroadcast")); + } else { + ui->broadcastButton->setText(QTStr( + "Basic.Main.AutoStopEnabled")); + ui->broadcastButton->setEnabled(false); + } + ui->broadcastButton->setProperty( + "broadcastState", "active"); + ui->broadcastButton->style()->unpolish( + ui->broadcastButton); + ui->broadcastButton->style()->polish( + ui->broadcastButton); + broadcastActive = true; + } + + bool recordWhenStreaming = config_get_bool( + GetGlobalConfig(), "BasicWindow", + "RecordWhenStreaming"); + if (recordWhenStreaming) + StartRecording(); + + bool replayBufferWhileStreaming = config_get_bool( + GetGlobalConfig(), "BasicWindow", + "ReplayBufferWhileStreaming"); + if (replayBufferWhileStreaming) + StartReplayBuffer(); #ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); + if (!autoStartBroadcast) + OBSBasic::ShowYouTubeAutoStartWarning(); #endif + }); + startStreamingFuture = {holder.cancelAll, future}; } void OBSBasic::BroadcastButtonClicked() @@ -7447,10 +7491,12 @@ void OBSBasic::StreamDelayStopping(int sec) void OBSBasic::StreamingStart() { + OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); + ui->streamButton->setText(QTStr("Basic.Main.StopStreaming")); ui->streamButton->setEnabled(true); ui->streamButton->setChecked(true); - ui->statusbar->StreamStarted(outputHandler->streamOutput); + ui->statusbar->StreamStarted(output); if (sysTrayStream) { sysTrayStream->setText(ui->streamButton->text()); diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 68b7248c3..64d237446 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,7 @@ #include "auth-base.hpp" #include "log-viewer.hpp" #include "undo-stack-obs.hpp" +#include "qt-helpers.hpp" #include @@ -284,6 +286,7 @@ private: OBSService service; std::unique_ptr outputHandler; + FutureHolder startStreamingFuture; bool streamingStopping = false; bool recordingStopping = false; bool replayBufferStopping = false; diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index e28c5b189..8a5f41c00 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "window-basic-settings.hpp" #include "obs-frontend-api.h" @@ -20,6 +21,13 @@ #include "youtube-api-wrappers.hpp" #endif +static const QUuid &CustomServerUUID() +{ + static const QUuid uuid = QUuid::fromString( + QT_UTF8("{241da255-70f2-4bbb-bef7-509695bf8e65}")); + return uuid; +} + struct QCef; struct QCefCookieManager; @@ -37,7 +45,7 @@ enum class Section : int { StreamKey, }; -inline bool OBSBasicSettings::IsCustomService() const +bool OBSBasicSettings::IsCustomService() const { return ui->service->currentData().toInt() == (int)ListOpt::Custom; } @@ -88,6 +96,16 @@ void OBSBasicSettings::InitStreamPage() &OBSBasicSettings::DisplayEnforceWarning); connect(ui->ignoreRecommended, &QCheckBox::toggled, this, &OBSBasicSettings::UpdateResFPSLimits); + + connect(ui->enableMultitrackVideo, &QCheckBox::toggled, this, + &OBSBasicSettings::UpdateMultitrackVideo); + connect(ui->multitrackVideoMaximumAggregateBitrateAuto, + &QCheckBox::toggled, this, + &OBSBasicSettings::UpdateMultitrackVideo); + connect(ui->multitrackVideoMaximumVideoTracksAuto, &QCheckBox::toggled, + this, &OBSBasicSettings::UpdateMultitrackVideo); + connect(ui->multitrackVideoConfigOverrideEnable, &QCheckBox::toggled, + this, &OBSBasicSettings::UpdateMultitrackVideo); } void OBSBasicSettings::LoadStream1Settings() @@ -108,6 +126,8 @@ void OBSBasicSettings::LoadStream1Settings() const char *service = obs_data_get_string(settings, "service"); const char *server = obs_data_get_string(settings, "server"); const char *key = obs_data_get_string(settings, "key"); + bool use_custom_server = + obs_data_get_bool(settings, "using_custom_server"); protocol = QT_UTF8(obs_service_get_protocol(service_obj)); const char *bearer_token = obs_data_get_string(settings, "bearer_token"); @@ -145,10 +165,54 @@ void OBSBasicSettings::LoadStream1Settings() ui->twitchAddonDropdown->setCurrentIndex(idx); } + ui->enableMultitrackVideo->setChecked(config_get_bool( + main->Config(), "Stream1", "EnableMultitrackVideo")); + + ui->multitrackVideoMaximumAggregateBitrateAuto->setChecked( + config_get_bool(main->Config(), "Stream1", + "MultitrackVideoMaximumAggregateBitrateAuto")); + if (config_has_user_value(main->Config(), "Stream1", + "MultitrackVideoMaximumAggregateBitrate")) { + ui->multitrackVideoMaximumAggregateBitrate->setValue( + config_get_int( + main->Config(), "Stream1", + "MultitrackVideoMaximumAggregateBitrate")); + } + + ui->multitrackVideoMaximumVideoTracksAuto->setChecked( + config_get_bool(main->Config(), "Stream1", + "MultitrackVideoMaximumVideoTracksAuto")); + if (config_has_user_value(main->Config(), "Stream1", + "MultitrackVideoMaximumVideoTracks")) + ui->multitrackVideoMaximumVideoTracks->setValue( + config_get_int(main->Config(), "Stream1", + "MultitrackVideoMaximumVideoTracks")); + + ui->multitrackVideoStreamDumpEnable->setChecked(config_get_bool( + main->Config(), "Stream1", "MultitrackVideoStreamDumpEnabled")); + + ui->multitrackVideoConfigOverrideEnable->setChecked( + config_get_bool(main->Config(), "Stream1", + "MultitrackVideoConfigOverrideEnabled")); + if (config_has_user_value(main->Config(), "Stream1", + "MultitrackVideoConfigOverride")) + ui->multitrackVideoConfigOverride->setPlainText( + DeserializeConfigText( + config_get_string( + main->Config(), "Stream1", + "MultitrackVideoConfigOverride")) + .c_str()); + UpdateServerList(); if (is_rtmp_common) { - int idx = ui->server->findData(server); + int idx = -1; + if (use_custom_server) { + idx = ui->server->findData(CustomServerUUID()); + } else { + idx = ui->server->findData(QString::fromUtf8(server)); + } + if (idx == -1) { if (server && *server) ui->server->insertItem(0, server, server); @@ -157,6 +221,9 @@ void OBSBasicSettings::LoadStream1Settings() ui->server->setCurrentIndex(idx); } + if (use_custom_server) + ui->serviceCustomServer->setText(server); + if (is_whip) ui->key->setText(bearer_token); else @@ -168,6 +235,7 @@ void OBSBasicSettings::LoadStream1Settings() UpdateMoreInfoLink(); UpdateVodTrackSetting(); UpdateServiceRecommendations(); + UpdateMultitrackVideo(); bool streamActive = obs_frontend_streaming_active(); ui->streamPage->setEnabled(!streamActive); @@ -223,9 +291,19 @@ void OBSBasicSettings::SaveStream1Settings() obs_data_set_string(settings, "service", QT_TO_UTF8(ui->service->currentText())); obs_data_set_string(settings, "protocol", QT_TO_UTF8(protocol)); - obs_data_set_string( - settings, "server", - QT_TO_UTF8(ui->server->currentData().toString())); + if (ui->server->currentData() == CustomServerUUID()) { + obs_data_set_bool(settings, "using_custom_server", + true); + + obs_data_set_string( + settings, "server", + QT_TO_UTF8(ui->serviceCustomServer->text())); + } else { + obs_data_set_string( + settings, "server", + QT_TO_UTF8( + ui->server->currentData().toString())); + } } else { obs_data_set_string( settings, "server", @@ -286,6 +364,49 @@ void OBSBasicSettings::SaveStream1Settings() } SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended"); + + auto oldMultitrackVideoSetting = config_get_bool( + main->Config(), "Stream1", "EnableMultitrackVideo"); + + if (!IsCustomService()) { + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "service", + QT_TO_UTF8(ui->service->currentText())); + OBSServiceAutoRelease temp_service = obs_service_create_private( + "rtmp_common", "auto config query service", settings); + settings = obs_service_get_settings(temp_service); + auto available = obs_data_has_user_value( + settings, "multitrack_video_configuration_url"); + + if (available) { + SaveCheckBox(ui->enableMultitrackVideo, "Stream1", + "EnableMultitrackVideo"); + } else { + config_remove_value(main->Config(), "Stream1", + "EnableMultitrackVideo"); + } + } else { + SaveCheckBox(ui->enableMultitrackVideo, "Stream1", + "EnableMultitrackVideo"); + } + SaveCheckBox(ui->multitrackVideoMaximumAggregateBitrateAuto, "Stream1", + "MultitrackVideoMaximumAggregateBitrateAuto"); + SaveSpinBox(ui->multitrackVideoMaximumAggregateBitrate, "Stream1", + "MultitrackVideoMaximumAggregateBitrate"); + SaveCheckBox(ui->multitrackVideoMaximumVideoTracksAuto, "Stream1", + "MultitrackVideoMaximumVideoTracksAuto"); + SaveSpinBox(ui->multitrackVideoMaximumVideoTracks, "Stream1", + "MultitrackVideoMaximumVideoTracks"); + SaveCheckBox(ui->multitrackVideoStreamDumpEnable, "Stream1", + "MultitrackVideoStreamDumpEnabled"); + SaveCheckBox(ui->multitrackVideoConfigOverrideEnable, "Stream1", + "MultitrackVideoConfigOverrideEnabled"); + SaveText(ui->multitrackVideoConfigOverride, "Stream1", + "MultitrackVideoConfigOverride"); + + if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked()) + main->ResetOutputs(); + SwapMultiTrack(QT_TO_UTF8(protocol)); } @@ -526,6 +647,7 @@ void OBSBasicSettings::on_service_currentIndexChanged(int idx) protocol = FindProtocol(); UpdateAdvNetworkGroup(); + UpdateMultitrackVideo(); if (ServiceSupportsCodecCheck() && UpdateResFPSLimits()) { lastServiceIdx = idx; @@ -547,6 +669,7 @@ void OBSBasicSettings::on_customServer_textChanged(const QString &) protocol = FindProtocol(); UpdateAdvNetworkGroup(); + UpdateMultitrackVideo(); if (ServiceSupportsCodecCheck()) lastCustomServer = ui->customServer->text(); @@ -567,6 +690,10 @@ void OBSBasicSettings::ServiceChanged(bool resetFields) if (resetFields || lastService != service.c_str()) { reset_service_ui_fields(ui.get(), service, loading); + + ui->enableMultitrackVideo->setChecked(config_get_bool( + main->Config(), "Stream1", "EnableMultitrackVideo")); + UpdateMultitrackVideo(); } ui->useAuth->setVisible(custom); @@ -576,8 +703,8 @@ void OBSBasicSettings::ServiceChanged(bool resetFields) ui->authPwWidget->setVisible(custom); if (custom || whip) { - ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, - ui->serverStackedWidget); + ui->destinationLayout->insertRow(1, ui->serverLabel, + ui->serverStackedWidget); ui->serverStackedWidget->setCurrentIndex(1); ui->serverStackedWidget->setVisible(true); @@ -675,6 +802,12 @@ void OBSBasicSettings::UpdateServerList() ui->server->addItem(name, server); } + if (serviceName == "Twitch") { + ui->server->addItem( + QTStr("Basic.Settings.Stream.SpecifyCustomServer"), + CustomServerUUID()); + } + obs_properties_destroy(props); } @@ -887,6 +1020,19 @@ void OBSBasicSettings::on_useAuth_toggled() ui->authPwWidget->setVisible(use_auth); } +bool OBSBasicSettings::IsCustomServer() +{ + return ui->server->currentData() == QVariant{CustomServerUUID()}; +} + +void OBSBasicSettings::on_server_currentIndexChanged(int /*index*/) +{ + auto server_is_custom = IsCustomServer(); + + ui->serviceCustomServerLabel->setVisible(server_is_custom); + ui->serviceCustomServer->setVisible(server_is_custom); +} + void OBSBasicSettings::UpdateVodTrackSetting() { bool enableForCustomServer = config_get_bool( diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 3a719d130..82cd64dcc 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -337,6 +337,7 @@ void RestrictResetBitrates(initializer_list boxes, int maxbitrate); #define GROUP_CHANGED &QGroupBox::toggled #define SCROLL_CHANGED &QSpinBox::valueChanged #define DSCROLL_CHANGED &QDoubleSpinBox::valueChanged +#define TEXT_CHANGED &QPlainTextEdit::textChanged #define GENERAL_CHANGED &OBSBasicSettings::GeneralChanged #define STREAM1_CHANGED &OBSBasicSettings::Stream1Changed @@ -411,6 +412,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->service, COMBO_CHANGED, STREAM1_CHANGED); HookWidget(ui->server, COMBO_CHANGED, STREAM1_CHANGED); HookWidget(ui->customServer, EDIT_CHANGED, STREAM1_CHANGED); + HookWidget(ui->serviceCustomServer, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->key, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->bandwidthTestEnable, CHECK_CHANGED, STREAM1_CHANGED); HookWidget(ui->twitchAddonDropdown, COMBO_CHANGED, STREAM1_CHANGED); @@ -418,6 +420,14 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->authUsername, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->authPw, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->ignoreRecommended, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->enableMultitrackVideo, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoMaximumAggregateBitrateAuto, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoMaximumAggregateBitrate, SCROLL_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoMaximumVideoTracksAuto, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoMaximumVideoTracks, SCROLL_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoStreamDumpEnable, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoConfigOverrideEnable, CHECK_CHANGED, STREAM1_CHANGED); + HookWidget(ui->multitrackVideoConfigOverride, TEXT_CHANGED, STREAM1_CHANGED); HookWidget(ui->outputMode, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutputPath, EDIT_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED); @@ -886,6 +896,8 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) obs_properties_destroy(ppts); + ui->multitrackVideoNoticeBox->setVisible(false); + InitStreamPage(); InitAppearancePage(); LoadSettings(false); @@ -1067,6 +1079,27 @@ void OBSBasicSettings::SaveSpinBox(QSpinBox *widget, const char *section, config_set_int(main->Config(), section, value, widget->value()); } +void OBSBasicSettings::SaveText(QPlainTextEdit *widget, const char *section, + const char *value) +{ + if (!WidgetChanged(widget)) + return; + + auto utf8 = widget->toPlainText().toUtf8(); + + OBSDataAutoRelease safe_text = obs_data_create(); + obs_data_set_string(safe_text, "text", utf8.constData()); + + config_set_string(main->Config(), section, value, + obs_data_get_json(safe_text)); +} + +std::string DeserializeConfigText(const char *value) +{ + OBSDataAutoRelease data = obs_data_create_from_json(value); + return obs_data_get_string(data, "text"); +} + void OBSBasicSettings::SaveGroupBox(QGroupBox *widget, const char *section, const char *value) { @@ -4631,6 +4664,8 @@ void OBSBasicSettings::OutputsChanged() outputsChanged = true; sender()->setProperty("changed", QVariant(true)); EnableApplyButton(true); + + UpdateMultitrackVideo(); } } @@ -5389,6 +5424,7 @@ void OBSBasicSettings::SimpleRecordingQualityChanged() ui->simpleOutRecFormat->setVisible(!losslessQuality); ui->simpleOutRecFormatLabel->setVisible(!losslessQuality); + UpdateMultitrackVideo(); SimpleRecordingEncoderChanged(); SimpleReplayBufferChanged(); } @@ -6246,6 +6282,244 @@ void OBSBasicSettings::UpdateAdvNetworkGroup() #endif } +extern bool MultitrackVideoDeveloperModeEnabled(); + +void OBSBasicSettings::UpdateMultitrackVideo() +{ + // Technically, it should currently be safe to toggle multitrackVideo + // while not streaming (recording should be irrelevant), but practically + // output settings aren't currently being tracked with that degree of + // flexibility, so just disable everything while outputs are active. + auto toggle_available = !main->Active(); + + // FIXME: protocol is not updated properly for WHIP; what do? + auto available = protocol.startsWith("RTMP"); + + if (available && !IsCustomService()) { + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_string(settings, "service", + QT_TO_UTF8(ui->service->currentText())); + OBSServiceAutoRelease temp_service = obs_service_create_private( + "rtmp_common", "auto config query service", settings); + settings = obs_service_get_settings(temp_service); + available = obs_data_has_user_value( + settings, "multitrack_video_configuration_url"); + if (!available && ui->enableMultitrackVideo->isChecked()) + ui->enableMultitrackVideo->setChecked(false); + } + + ui->multitrackVideoGroupBox->setVisible(available); + + ui->enableMultitrackVideo->setEnabled(toggle_available); + + ui->multitrackVideoMaximumAggregateBitrateLabel->setEnabled( + toggle_available && ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoMaximumAggregateBitrateAuto->setEnabled( + toggle_available && ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoMaximumAggregateBitrate->setEnabled( + toggle_available && ui->enableMultitrackVideo->isChecked() && + !ui->multitrackVideoMaximumAggregateBitrateAuto->isChecked()); + + ui->multitrackVideoMaximumVideoTracksLabel->setEnabled( + toggle_available && ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoMaximumVideoTracksAuto->setEnabled( + toggle_available && ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoMaximumVideoTracks->setEnabled( + toggle_available && ui->enableMultitrackVideo->isChecked() && + !ui->multitrackVideoMaximumVideoTracksAuto->isChecked()); + + ui->multitrackVideoStreamDumpEnable->setVisible( + available && MultitrackVideoDeveloperModeEnabled()); + ui->multitrackVideoConfigOverrideEnable->setVisible( + available && MultitrackVideoDeveloperModeEnabled()); + ui->multitrackVideoConfigOverrideLabel->setVisible( + available && MultitrackVideoDeveloperModeEnabled()); + ui->multitrackVideoConfigOverride->setVisible( + available && MultitrackVideoDeveloperModeEnabled()); + + ui->multitrackVideoStreamDumpEnable->setEnabled( + toggle_available && ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoConfigOverrideEnable->setEnabled( + toggle_available && ui->enableMultitrackVideo->isChecked()); + ui->multitrackVideoConfigOverrideLabel->setEnabled( + toggle_available && ui->enableMultitrackVideo->isChecked() && + ui->multitrackVideoConfigOverrideEnable->isChecked()); + ui->multitrackVideoConfigOverride->setEnabled( + toggle_available && ui->enableMultitrackVideo->isChecked() && + ui->multitrackVideoConfigOverrideEnable->isChecked()); + + auto update_simple_output_settings = [&](bool mtv_enabled) { + auto recording_uses_stream_encoder = + ui->simpleOutRecQuality->currentData().toString() == + "Stream"; + mtv_enabled = mtv_enabled && !recording_uses_stream_encoder; + + ui->simpleOutputVBitrateLabel->setDisabled(mtv_enabled); + ui->simpleOutputVBitrate->setDisabled(mtv_enabled); + + ui->simpleOutputABitrateLabel->setDisabled(mtv_enabled); + ui->simpleOutputABitrate->setDisabled(mtv_enabled); + + ui->simpleOutStrEncoderLabel->setDisabled(mtv_enabled); + ui->simpleOutStrEncoder->setDisabled(mtv_enabled); + + ui->simpleOutPresetLabel->setDisabled(mtv_enabled); + ui->simpleOutPreset->setDisabled(mtv_enabled); + + ui->simpleOutCustomLabel->setDisabled(mtv_enabled); + ui->simpleOutCustom->setDisabled(mtv_enabled); + + ui->simpleOutStrAEncoderLabel->setDisabled(mtv_enabled); + ui->simpleOutStrAEncoder->setDisabled(mtv_enabled); + }; + + auto update_advanced_output_settings = [&](bool mtv_enabled) { + auto recording_uses_stream_video_encoder = + ui->advOutRecEncoder->currentText() == + TEXT_USE_STREAM_ENC; + auto recording_uses_stream_audio_encoder = + ui->advOutRecAEncoder->currentData() == "none"; + auto disable_video = mtv_enabled && + !recording_uses_stream_video_encoder; + auto disable_audio = mtv_enabled && + !recording_uses_stream_audio_encoder; + + ui->advOutAEncLabel->setDisabled(disable_audio); + ui->advOutAEncoder->setDisabled(disable_audio); + + ui->advOutEncLabel->setDisabled(disable_video); + ui->advOutEncoder->setDisabled(disable_video); + + ui->advOutUseRescale->setDisabled(disable_video); + ui->advOutRescale->setDisabled(disable_video); + ui->advOutRescaleFilter->setDisabled(disable_video); + + if (streamEncoderProps) + streamEncoderProps->SetDisabled(disable_video); + }; + + auto update_advanced_output_audio_tracks = [&](bool mtv_enabled) { + auto vod_track_enabled = vodTrackCheckbox && + vodTrackCheckbox->isChecked(); + + auto vod_track_idx_enabled = [&](size_t idx) { + return vod_track_enabled && vodTrack[idx] && + vodTrack[idx]->isChecked(); + }; + + auto track1_warning_visible = mtv_enabled && + (ui->advOutTrack1->isChecked() || + vod_track_idx_enabled(1)); + auto track1_disabled = track1_warning_visible && + !ui->advOutRecTrack1->isChecked(); + ui->advOutTrack1BitrateLabel->setDisabled(track1_disabled); + ui->advOutTrack1Bitrate->setDisabled(track1_disabled); + + auto track2_warning_visible = mtv_enabled && + (ui->advOutTrack2->isChecked() || + vod_track_idx_enabled(2)); + auto track2_disabled = track2_warning_visible && + !ui->advOutRecTrack2->isChecked(); + ui->advOutTrack2BitrateLabel->setDisabled(track2_disabled); + ui->advOutTrack2Bitrate->setDisabled(track2_disabled); + + auto track3_warning_visible = mtv_enabled && + (ui->advOutTrack3->isChecked() || + vod_track_idx_enabled(3)); + auto track3_disabled = track3_warning_visible && + !ui->advOutRecTrack3->isChecked(); + ui->advOutTrack3BitrateLabel->setDisabled(track3_disabled); + ui->advOutTrack3Bitrate->setDisabled(track3_disabled); + + auto track4_warning_visible = mtv_enabled && + (ui->advOutTrack4->isChecked() || + vod_track_idx_enabled(4)); + auto track4_disabled = track4_warning_visible && + !ui->advOutRecTrack4->isChecked(); + ui->advOutTrack4BitrateLabel->setDisabled(track4_disabled); + ui->advOutTrack4Bitrate->setDisabled(track4_disabled); + + auto track5_warning_visible = mtv_enabled && + (ui->advOutTrack5->isChecked() || + vod_track_idx_enabled(5)); + auto track5_disabled = track5_warning_visible && + !ui->advOutRecTrack5->isChecked(); + ui->advOutTrack5BitrateLabel->setDisabled(track5_disabled); + ui->advOutTrack5Bitrate->setDisabled(track5_disabled); + + auto track6_warning_visible = mtv_enabled && + (ui->advOutTrack6->isChecked() || + vod_track_idx_enabled(6)); + auto track6_disabled = track6_warning_visible && + !ui->advOutRecTrack6->isChecked(); + ui->advOutTrack6BitrateLabel->setDisabled(track6_disabled); + ui->advOutTrack6Bitrate->setDisabled(track6_disabled); + }; + + if (available) { + OBSDataAutoRelease settings; + { + auto service_name = ui->service->currentText(); + auto custom_server = ui->customServer->text().trimmed(); + + obs_properties_t *props = + obs_get_service_properties("rtmp_common"); + obs_property_t *service = + obs_properties_get(props, "service"); + + settings = obs_data_create(); + + obs_data_set_string(settings, "service", + QT_TO_UTF8(service_name)); + obs_property_modified(service, settings); + + obs_properties_destroy(props); + } + + auto multitrack_video_name = + QTStr("Basic.Settings.Stream.MultitrackVideoLabel"); + if (obs_data_has_user_value(settings, "multitrack_video_name")) + multitrack_video_name = obs_data_get_string( + settings, "multitrack_video_name"); + + ui->enableMultitrackVideo->setText( + QTStr("Basic.Settings.Stream.EnableMultitrackVideo") + .arg(multitrack_video_name)); + + if (obs_data_has_user_value(settings, + "multitrack_video_disclaimer")) { + ui->multitrackVideoInfo->setVisible(true); + ui->multitrackVideoInfo->setText(obs_data_get_string( + settings, "multitrack_video_disclaimer")); + } else { + ui->multitrackVideoInfo->setText( + QTStr("MultitrackVideo.Info") + .arg(multitrack_video_name, + ui->service->currentText())); + } + + auto disabled_text = + QTStr("Basic.Settings.MultitrackVideoDisabledSettings") + .arg(ui->service->currentText()) + .arg(multitrack_video_name); + + ui->multitrackVideoNotice->setText(disabled_text); + + auto mtv_enabled = ui->enableMultitrackVideo->isChecked(); + ui->multitrackVideoNoticeBox->setVisible(mtv_enabled); + + update_simple_output_settings(mtv_enabled); + update_advanced_output_settings(mtv_enabled); + update_advanced_output_audio_tracks(mtv_enabled); + } else { + ui->multitrackVideoNoticeBox->setVisible(false); + + update_simple_output_settings(false); + update_advanced_output_settings(false); + update_advanced_output_audio_tracks(false); + } +} + void OBSBasicSettings::SimpleStreamAudioEncoderChanged() { PopulateSimpleBitrates( diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index e15c25934..846b80014 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -70,6 +70,8 @@ public slots: } }; +std::string DeserializeConfigText(const char *value); + class OBSBasicSettings : public QDialog { Q_OBJECT Q_PROPERTY(QIcon generalIcon READ GetGeneralIcon WRITE SetGeneralIcon @@ -193,6 +195,8 @@ private: const char *value); void SaveSpinBox(QSpinBox *widget, const char *section, const char *value); + void SaveText(QPlainTextEdit *widget, const char *section, + const char *value); void SaveFormat(QComboBox *combo); void SaveEncoder(QComboBox *combo, const char *section, const char *value); @@ -273,7 +277,7 @@ private: /* stream */ void InitStreamPage(); - inline bool IsCustomService() const; + bool IsCustomService() const; inline bool IsWHIP() const; void LoadServices(bool showAll); void OnOAuthStreamKeyConnected(); @@ -296,7 +300,10 @@ private: /* Appearance */ void InitAppearancePage(); + bool IsCustomServer(); + private slots: + void UpdateMultitrackVideo(); void RecreateOutputResolutionWidget(); bool UpdateResFPSLimits(); void DisplayEnforceWarning(bool checked); @@ -306,6 +313,7 @@ private slots: void on_disconnectAccount_clicked(); void on_useStreamKey_clicked(); void on_useAuth_toggled(); + void on_server_currentIndexChanged(int index); void on_hotkeyFilterReset_clicked(); void on_hotkeyFilterSearch_textChanged(const QString text);