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 2b6b1fc12..8a24e7265 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -721,6 +721,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" @@ -1543,3 +1544,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/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/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.cpp b/UI/window-basic-auto-config.cpp index f992c0719..e06e65eca 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -3,6 +3,8 @@ #include +#include + #include "window-basic-auto-config.hpp" #include "window-basic-main.hpp" #include "qt-wrappers.hpp" 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.cpp b/UI/window-basic-settings.cpp index 775d13098..f61c02e2a 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -1068,6 +1068,12 @@ void OBSBasicSettings::SaveSpinBox(QSpinBox *widget, const char *section, config_set_int(main->Config(), section, value, widget->value()); } +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) { diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index 51830c63f..03d55a263 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