UI: Add eRTMP Multitrack Video Output
This commit is contained in:
parent
43a1b30994
commit
c8950900c3
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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}"
|
||||
"$<$<BOOL:${LINUX_PORTABLE}>:LINUX_PORTABLE>")
|
||||
if(TARGET obspython)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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<br><br>HTTP error: %2"
|
||||
FailedToStartStream.WarningUnknownStatus="Received unknown status value '%1'"
|
||||
FailedToStartStream.WarningRetryNonMultitrackVideo="\n<br><br>\nDo you want to continue streaming without %1?"
|
||||
FailedToStartStream.WarningRetry="\n<br><br>\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"
|
||||
|
89
UI/goliveapi-censoredjson.cpp
Normal file
89
UI/goliveapi-censoredjson.cpp
Normal file
@ -0,0 +1,89 @@
|
||||
#include "goliveapi-censoredjson.hpp"
|
||||
#include <unordered_map>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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));
|
||||
}
|
12
UI/goliveapi-censoredjson.hpp
Normal file
12
UI/goliveapi-censoredjson.hpp
Normal file
@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <obs.hpp>
|
||||
#include <QString>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
/**
|
||||
* 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);
|
146
UI/goliveapi-network.cpp
Normal file
146
UI/goliveapi-network.cpp
Normal file
@ -0,0 +1,146 @@
|
||||
#include "goliveapi-network.hpp"
|
||||
#include "goliveapi-censoredjson.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
#include <obs-app.hpp>
|
||||
#include <remote-text.hpp>
|
||||
#include "multitrack-video-error.hpp"
|
||||
|
||||
#include <qstring.h>
|
||||
#include <string>
|
||||
#include <QMessageBox>
|
||||
#include <QThreadPool>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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<std::string> 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;
|
||||
}
|
16
UI/goliveapi-network.hpp
Normal file
16
UI/goliveapi-network.hpp
Normal file
@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <obs.hpp>
|
||||
#include <QFuture>
|
||||
#include <QString>
|
||||
|
||||
#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);
|
47
UI/goliveapi-postdata.cpp
Normal file
47
UI/goliveapi-postdata.cpp
Normal file
@ -0,0 +1,47 @@
|
||||
#include "goliveapi-postdata.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "system-info.hpp"
|
||||
|
||||
#include "models/multitrack-video.hpp"
|
||||
|
||||
GoLiveApi::PostData
|
||||
constructGoLivePost(QString streamKey,
|
||||
const std::optional<uint64_t> &maximum_aggregate_bitrate,
|
||||
const std::optional<uint32_t> &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;
|
||||
}
|
12
UI/goliveapi-postdata.hpp
Normal file
12
UI/goliveapi-postdata.hpp
Normal file
@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <obs.hpp>
|
||||
#include <optional>
|
||||
#include <QString>
|
||||
#include "models/multitrack-video.hpp"
|
||||
|
||||
GoLiveApi::PostData
|
||||
constructGoLivePost(QString streamKey,
|
||||
const std::optional<uint64_t> &maximum_aggregate_bitrate,
|
||||
const std::optional<uint32_t> &maximum_video_tracks,
|
||||
bool vod_track_enabled);
|
323
UI/models/multitrack-video.hpp
Normal file
323
UI/models/multitrack-video.hpp
Normal file
@ -0,0 +1,323 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Ruwen Hahn <haruwenz@twitch.tv>
|
||||
*
|
||||
* 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 <string>
|
||||
#include <optional>
|
||||
|
||||
#include <obs.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
/* 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<typename T> struct nlohmann::adl_serializer<std::optional<T>> {
|
||||
static void from_json(const json &j, std::optional<T> &opt)
|
||||
{
|
||||
if (j.is_null()) {
|
||||
opt = std::nullopt;
|
||||
} else {
|
||||
opt = j.get<T>();
|
||||
}
|
||||
}
|
||||
static void to_json(json &json, std::optional<T> 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<uint32_t> speed;
|
||||
optional<string> 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<string> driver_version;
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(Gpu, model, vendor_id, device_id,
|
||||
dedicated_video_memory,
|
||||
shared_system_memory, luid,
|
||||
driver_version)
|
||||
};
|
||||
|
||||
struct GamingFeatures {
|
||||
optional<bool> game_bar_enabled;
|
||||
optional<bool> game_dvr_allowed;
|
||||
optional<bool> game_dvr_enabled;
|
||||
optional<bool> game_dvr_bg_recording;
|
||||
optional<bool> game_mode_enabled;
|
||||
optional<bool> 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<GamingFeatures> gaming_features;
|
||||
System system;
|
||||
optional<std::vector<Gpu>> gpu;
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE(Capabilities, client, cpu, memory,
|
||||
gaming_features, system, gpu)
|
||||
};
|
||||
|
||||
struct Preferences {
|
||||
optional<uint64_t> maximum_aggregate_bitrate;
|
||||
optional<uint32_t> 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<string> html_en_us;
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Status, result, html_en_us)
|
||||
};
|
||||
|
||||
struct IngestEndpoint {
|
||||
string protocol;
|
||||
string url_template;
|
||||
optional<string> 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<media_frames_per_second> framerate;
|
||||
optional<obs_scale_type> 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<typename T> struct EncoderConfiguration {
|
||||
T config;
|
||||
json data;
|
||||
|
||||
friend void to_json(nlohmann::json &nlohmann_json_j,
|
||||
const EncoderConfiguration<T> &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<T> &nlohmann_json_t)
|
||||
{
|
||||
nlohmann_json_t.data = nlohmann_json_j;
|
||||
nlohmann_json_j.get_to(nlohmann_json_t.config);
|
||||
}
|
||||
};
|
||||
|
||||
struct AudioConfigurations {
|
||||
std::vector<EncoderConfiguration<AudioEncoderConfiguration>> live;
|
||||
std::vector<EncoderConfiguration<AudioEncoderConfiguration>> vod;
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(AudioConfigurations, live,
|
||||
vod)
|
||||
};
|
||||
|
||||
struct Config {
|
||||
Meta meta;
|
||||
optional<Status> status;
|
||||
std::vector<IngestEndpoint> ingest_endpoints;
|
||||
std::vector<EncoderConfiguration<VideoEncoderConfiguration>>
|
||||
encoder_configurations;
|
||||
AudioConfigurations audio_configurations;
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Config, meta, status,
|
||||
ingest_endpoints,
|
||||
encoder_configurations,
|
||||
audio_configurations)
|
||||
};
|
||||
} // namespace GoLiveApi
|
46
UI/multitrack-video-error.cpp
Normal file
46
UI/multitrack-video-error.cpp
Normal file
@ -0,0 +1,46 @@
|
||||
#include "multitrack-video-error.hpp"
|
||||
|
||||
#include <QMessageBox>
|
||||
#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;
|
||||
}
|
22
UI/multitrack-video-error.hpp
Normal file
22
UI/multitrack-video-error.hpp
Normal file
@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
|
||||
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;
|
||||
};
|
950
UI/multitrack-video-output.cpp
Normal file
950
UI/multitrack-video-output.cpp
Normal file
@ -0,0 +1,950 @@
|
||||
#include "multitrack-video-output.hpp"
|
||||
|
||||
#include <util/dstr.hpp>
|
||||
#include <util/platform.h>
|
||||
#include <util/profiler.hpp>
|
||||
#include <util/util.hpp>
|
||||
#include <obs-frontend-api.h>
|
||||
#include <obs-app.hpp>
|
||||
#include <obs.hpp>
|
||||
#include <remote-text.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QMessageBox>
|
||||
#include <QObject>
|
||||
#include <QPushButton>
|
||||
#include <QScopeGuard>
|
||||
#include <QString>
|
||||
#include <QThreadPool>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QUuid>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#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<std::string> &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<char>{
|
||||
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<uint32_t>(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<const char *> &get_available_encoders()
|
||||
{
|
||||
// encoders are currently only registered during startup, so keeping
|
||||
// a static vector around shouldn't be a problem
|
||||
static std::vector<const char *> available_encoders = [] {
|
||||
std::vector<const char *> 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<OBSEncoderAutoRelease> &audio_encoders,
|
||||
std::vector<OBSEncoderAutoRelease> &video_encoders,
|
||||
const char *audio_encoder_id,
|
||||
std::optional<size_t> 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<std::string> &rtmp_url, const QString &stream_key,
|
||||
const char *audio_encoder_id,
|
||||
std::optional<uint32_t> maximum_aggregate_bitrate,
|
||||
std::optional<uint32_t> maximum_video_tracks,
|
||||
std::optional<std::string> custom_config,
|
||||
obs_data_t *dump_stream_to_file_config,
|
||||
std::optional<size_t> vod_track_mixer)
|
||||
{
|
||||
{
|
||||
const std::lock_guard<std::mutex> current_lock{current_mutex};
|
||||
const std::lock_guard<std::mutex> 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<GoLiveApi::Config> go_live_config;
|
||||
std::optional<GoLiveApi::Config> 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<OBSEncoderAutoRelease>();
|
||||
auto video_encoders = std::vector<OBSEncoderAutoRelease>();
|
||||
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<OBSEncoderAutoRelease> &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<OBSEncoderAutoRelease> &audio_encoders,
|
||||
obs_output_t *output, obs_output_t *recording_output,
|
||||
const char *audio_encoder_id,
|
||||
std::optional<size_t> 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<OBSEncoderAutoRelease> &audio_encoders,
|
||||
std::vector<OBSEncoderAutoRelease> &video_encoders,
|
||||
const char *audio_encoder_id,
|
||||
std::optional<size_t> 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::OBSOutputObjects>
|
||||
MultitrackVideoOutput::take_current()
|
||||
{
|
||||
const std::lock_guard<std::mutex> current_lock{current_mutex};
|
||||
auto val = std::move(current);
|
||||
current.reset();
|
||||
return val;
|
||||
}
|
||||
|
||||
std::optional<MultitrackVideoOutput::OBSOutputObjects>
|
||||
MultitrackVideoOutput::take_current_stream_dump()
|
||||
{
|
||||
const std::lock_guard<std::mutex> 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<OBSOutputObjects> 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<MultitrackVideoOutput *>(arg);
|
||||
|
||||
OBSOutputAutoRelease stream_dump_output;
|
||||
{
|
||||
const std::lock_guard<std::mutex> 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<obs_output_t *>(
|
||||
calldata_ptr(params, "output"))))
|
||||
return;
|
||||
|
||||
MultitrackVideoOutput::ReleaseOnMainThread(self->take_current());
|
||||
}
|
||||
|
||||
void StreamDeactivateHandler(void *arg, calldata_t *params)
|
||||
{
|
||||
auto self = static_cast<MultitrackVideoOutput *>(arg);
|
||||
|
||||
if (obs_output_reconnecting(static_cast<obs_output_t *>(
|
||||
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<MultitrackVideoOutput *>(arg);
|
||||
blog(LOG_INFO, "MultitrackVideoOutput: recording stopped");
|
||||
|
||||
if (obs_output_active(static_cast<obs_output_t *>(
|
||||
calldata_ptr(params, "output"))))
|
||||
return;
|
||||
|
||||
MultitrackVideoOutput::ReleaseOnMainThread(
|
||||
self->take_current_stream_dump());
|
||||
}
|
||||
|
||||
void RecordingDeactivateHandler(void *arg, calldata_t * /*data*/)
|
||||
{
|
||||
auto self = static_cast<MultitrackVideoOutput *>(arg);
|
||||
MultitrackVideoOutput::ReleaseOnMainThread(
|
||||
self->take_current_stream_dump());
|
||||
}
|
79
UI/multitrack-video-output.hpp
Normal file
79
UI/multitrack-video-output.hpp
Normal file
@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include <obs.hpp>
|
||||
#include <util/config-file.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <qobject.h>
|
||||
#include <QFuture>
|
||||
#include <QFutureSynchronizer>
|
||||
|
||||
#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<std::string> &rtmp_url,
|
||||
const QString &stream_key,
|
||||
const char *audio_encoder_id,
|
||||
std::optional<uint32_t> maximum_aggregate_bitrate,
|
||||
std::optional<uint32_t> maximum_video_tracks,
|
||||
std::optional<std::string> custom_config,
|
||||
obs_data_t *dump_stream_to_file_config,
|
||||
std::optional<size_t> 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<OBSEncoderAutoRelease> video_encoders_;
|
||||
std::vector<OBSEncoderAutoRelease> audio_encoders_;
|
||||
OBSServiceAutoRelease multitrack_video_service_;
|
||||
OBSSignal start_signal, stop_signal, deactivate_signal;
|
||||
};
|
||||
|
||||
std::optional<OBSOutputObjects> take_current();
|
||||
std::optional<OBSOutputObjects> take_current_stream_dump();
|
||||
|
||||
static void
|
||||
ReleaseOnMainThread(std::optional<OBSOutputObjects> objects);
|
||||
|
||||
std::mutex current_mutex;
|
||||
std::optional<OBSOutputObjects> current;
|
||||
|
||||
std::mutex current_stream_dump_mutex;
|
||||
std::optional<OBSOutputObjects> 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);
|
||||
};
|
10
UI/qt-helpers.cpp
Normal file
10
UI/qt-helpers.cpp
Normal file
@ -0,0 +1,10 @@
|
||||
#include "qt-helpers.hpp"
|
||||
|
||||
QFuture<void> CreateFuture()
|
||||
{
|
||||
QPromise<void> promise;
|
||||
auto future = promise.future();
|
||||
promise.start();
|
||||
promise.finish();
|
||||
return future;
|
||||
}
|
46
UI/qt-helpers.hpp
Normal file
46
UI/qt-helpers.hpp
Normal file
@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <QFuture>
|
||||
#include <QtGlobal>
|
||||
|
||||
template<typename T> struct FutureHolder {
|
||||
std::function<void()> cancelAll;
|
||||
QFuture<T> future;
|
||||
};
|
||||
|
||||
QFuture<void> CreateFuture();
|
||||
|
||||
template<typename T> inline QFuture<T> PreventFutureDeadlock(QFuture<T> 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
|
||||
}
|
6
UI/system-info-macos.mm
Normal file
6
UI/system-info-macos.mm
Normal file
@ -0,0 +1,6 @@
|
||||
#include "system-info.hpp"
|
||||
|
||||
void system_info(GoLiveApi::Capabilities &capabilities)
|
||||
{
|
||||
UNUSED_PARAMETER(capabilities);
|
||||
}
|
6
UI/system-info-posix.cpp
Normal file
6
UI/system-info-posix.cpp
Normal file
@ -0,0 +1,6 @@
|
||||
#include "system-info.hpp"
|
||||
|
||||
void system_info(GoLiveApi::Capabilities &capabilities)
|
||||
{
|
||||
UNUSED_PARAMETER(capabilities);
|
||||
}
|
278
UI/system-info-windows.cpp
Normal file
278
UI/system-info-windows.cpp
Normal file
@ -0,0 +1,278 @@
|
||||
#include "system-info.hpp"
|
||||
|
||||
#include <dxgi.h>
|
||||
#include <cinttypes>
|
||||
#include <shlobj.h>
|
||||
|
||||
#include <util/dstr.hpp>
|
||||
#include <util/platform.h>
|
||||
#include <util/windows/ComPtr.hpp>
|
||||
#include <util/windows/win-registry.h>
|
||||
#include <util/windows/win-version.h>
|
||||
|
||||
static std::optional<std::vector<GoLiveApi::Gpu>> system_gpu_data()
|
||||
{
|
||||
ComPtr<IDXGIFactory1> factory;
|
||||
ComPtr<IDXGIAdapter1> adapter;
|
||||
HRESULT hr;
|
||||
UINT i;
|
||||
|
||||
hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory));
|
||||
if (FAILED(hr))
|
||||
return std::nullopt;
|
||||
|
||||
std::vector<GoLiveApi::Gpu> 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<GoLiveApi::GamingFeatures>
|
||||
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<bool> *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();
|
||||
}
|
||||
}
|
5
UI/system-info.hpp
Normal file
5
UI/system-info.hpp
Normal file
@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "models/multitrack-video.hpp"
|
||||
|
||||
void system_info(GoLiveApi::Capabilities &capabilities);
|
@ -3,6 +3,8 @@
|
||||
|
||||
#include <obs.hpp>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "window-basic-auto-config.hpp"
|
||||
#include "window-basic-main.hpp"
|
||||
#include "qt-wrappers.hpp"
|
||||
|
@ -1,8 +1,11 @@
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <QMessageBox>
|
||||
#include <QPromise>
|
||||
#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<MultitrackVideoOutput>();
|
||||
}
|
||||
|
||||
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<bool>
|
||||
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<bool> 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<bool>
|
||||
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<size_t> 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<bool>
|
||||
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<size_t>
|
||||
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<bool> 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<bool>
|
||||
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<std::optional<bool>>
|
||||
BasicOutputHandler::SetupMultitrackVideo(obs_service_t *service,
|
||||
std::string audio_encoder_id,
|
||||
std::optional<size_t> vod_track_mixer)
|
||||
{
|
||||
if (!multitrackVideo)
|
||||
return {[] {}, CreateFuture().then([] {
|
||||
return std::optional<bool>{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<std::string> 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 = "<unknown>";
|
||||
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<std::string> 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<uint32_t>(config_get_int(
|
||||
main->Config(), "Stream1",
|
||||
"MultitrackVideoMaximumAggregateBitrate"));
|
||||
|
||||
auto maximum_video_tracks =
|
||||
config_get_bool(main->Config(), "Stream1",
|
||||
"MultitrackVideoMaximumVideoTracksAuto")
|
||||
? std::nullopt
|
||||
: std::make_optional<uint32_t>(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<MultitrackVideoError> {
|
||||
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<MultitrackVideoError> error)
|
||||
-> std::optional<bool> {
|
||||
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);
|
||||
|
@ -1,7 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QFuture>
|
||||
|
||||
#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<MultitrackVideoOutput> 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<bool> 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<std::optional<bool>>
|
||||
SetupMultitrackVideo(obs_service_t *service,
|
||||
std::string audio_encoder_id,
|
||||
std::optional<size_t> vod_track_mixer);
|
||||
OBSDataAutoRelease GenerateMultitrackVideoStreamDumpConfig();
|
||||
};
|
||||
|
||||
BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main);
|
||||
|
@ -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());
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include <QWidgetAction>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QFuture>
|
||||
#include <obs.hpp>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
@ -42,6 +43,7 @@
|
||||
#include "auth-base.hpp"
|
||||
#include "log-viewer.hpp"
|
||||
#include "undo-stack-obs.hpp"
|
||||
#include "qt-helpers.hpp"
|
||||
|
||||
#include <obs-frontend-internal.hpp>
|
||||
|
||||
@ -284,6 +286,7 @@ private:
|
||||
|
||||
OBSService service;
|
||||
std::unique_ptr<BasicOutputHandler> outputHandler;
|
||||
FutureHolder<void> startStreamingFuture;
|
||||
bool streamingStopping = false;
|
||||
bool recordingStopping = false;
|
||||
bool replayBufferStopping = false;
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user