Files
obs-studio/frontend/utility/MultitrackVideoOutput.cpp
PatTheMav e35b16cba9 frontend: Enable multitrack RTMP option for custom RTMP services
This removes the need to provide an undocumented launch argument to
enable custom RTMP service configurations to use multitrack encoding
and also provide a custom configuration.
2025-09-26 01:48:39 -04:00

933 lines
33 KiB
C++

#include "MultitrackVideoError.hpp"
#include "MultitrackVideoOutput.hpp"
#include "models/multitrack-video.hpp"
#include "GoLiveAPI_Network.hpp"
#include "GoLiveAPI_PostData.hpp"
#include <OBSApp.hpp>
#include <bpm.h>
#include <util/dstr.hpp>
#include <libavformat/avformat.h>
#include <QPushButton>
#include <QMessageBox>
#include <QThread>
#include <QUrl>
#include <QUrlQuery>
#include <cinttypes>
// Codec profile strings
static const char *h264_main = "Main";
static const char *h264_high = "High";
static const char *h264_cb = "Constrained Baseline";
static const char *hevc_main = "Main";
static const char *hevc_main10 = "Main 10";
static const char *av1_main = "Main";
// Maximum reconnect attempts with an invalid key error before giving up (roughly 30 seconds with default start value)
static constexpr uint8_t MAX_RECONNECT_ATTEMPTS = 5;
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,
std::optional<bool> use_rtmps)
{
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;
if (use_rtmps.has_value() && *use_rtmps != (qstricmp("RTMPS", endpoint.protocol.c_str()) == 0))
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));
}
/* The stream key itself may contain query parameters, such as
* "bandwidthtest" that need to be carried over. */
QUrl parsed_user_key{in_stream_key};
QUrlQuery user_key_query{parsed_user_key};
QUrl parsed_key{stream_key};
QUrl parsed_url{url};
QUrlQuery parsed_query{parsed_url};
for (const auto &[key, value] : user_key_query.queryItems())
parsed_query.addQueryItem(key, value);
if (!go_live_config.meta.config_id.empty()) {
parsed_query.addQueryItem("clientConfigId", QString::fromStdString(go_live_config.meta.config_id));
}
parsed_key.setQuery(parsed_query);
OBSDataAutoRelease settings = obs_data_create();
obs_data_set_string(settings, "server", str->array);
obs_data_set_string(settings, "key", parsed_key.toString().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 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;
bool useMP4 = obs_data_get_bool(settings, "use_mp4");
if (useMP4) {
output = obs_output_create("mp4_output", "mp4 multitrack video", settings, nullptr);
} else {
output = obs_output_create("flv_output", "flv multitrack video", settings, nullptr);
}
if (!output) {
blog(LOG_ERROR, "Failed to create multitrack video %s output", useMP4 ? "mp4" : "flv");
}
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.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));
obs_encoder_set_preferred_video_format(video_encoder, encoder_config.format.value_or(VIDEO_FORMAT_NV12));
obs_encoder_set_preferred_color_space(video_encoder, encoder_config.colorspace.value_or(VIDEO_CS_709));
obs_encoder_set_preferred_range(video_encoder, encoder_config.range.value_or(VIDEO_RANGE_PARTIAL));
}
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 bool encoder_available(const char *type)
{
const char *id = nullptr;
for (size_t idx = 0; obs_enum_encoder_types(idx, &id); idx++) {
if (strcmp(id, type) == 0)
return true;
}
return false;
}
static OBSEncoderAutoRelease create_video_encoder(DStr &name_buffer, size_t encoder_index,
const GoLiveApi::VideoEncoderConfiguration &encoder_config,
const OBSCanvasAutoRelease &canvas)
{
auto encoder_type = encoder_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.settings.dump().c_str());
/* VAAPI-based encoders unfortunately use an integer for "profile". Until a string-based "profile" can be used with
* VAAPI, find the corresponding integer value and update the settings with an integer-based "profile".
*/
if (strstr(encoder_type, "vaapi")) {
// Move the "profile" string to "profile_str".
const char *profile_str = obs_data_get_string(encoder_settings, "profile");
obs_data_set_string(encoder_settings, "profile_str", profile_str);
obs_data_item_t *profile_item = obs_data_item_byname(encoder_settings, "profile");
obs_data_item_remove(&profile_item);
obs_data_item_release(&profile_item);
// Find the vaapi_profile integer based on codec type and "profile" string.
int vaapi_profile;
const char *codec = obs_get_encoder_codec(encoder_type);
if (strcmp(codec, "h264") == 0) {
if (astrcmpi(profile_str, h264_main) == 0) {
vaapi_profile = AV_PROFILE_H264_MAIN;
} else if (astrcmpi(profile_str, h264_high) == 0) {
vaapi_profile = AV_PROFILE_H264_HIGH;
} else if (astrcmpi(profile_str, h264_cb) == 0) {
vaapi_profile = AV_PROFILE_H264_CONSTRAINED_BASELINE;
} else {
blog(LOG_WARNING, "Unsupported H264 profile '%s', setting to Main profile",
profile_str);
vaapi_profile = AV_PROFILE_H264_MAIN;
}
} else if (strcmp(codec, "hevc") == 0) {
if (astrcmpi(profile_str, hevc_main) == 0) {
vaapi_profile = AV_PROFILE_HEVC_MAIN;
} else if (astrcmpi(profile_str, hevc_main10) == 0) {
vaapi_profile = AV_PROFILE_HEVC_MAIN_10;
} else {
blog(LOG_WARNING, "Unsupported HEVC profile '%s', setting to Main profile",
profile_str);
vaapi_profile = AV_PROFILE_HEVC_MAIN;
}
} else if (strcmp(codec, "av1") == 0) {
if (astrcmpi(profile_str, av1_main) == 0) {
vaapi_profile = AV_PROFILE_AV1_MAIN;
} else {
blog(LOG_WARNING, "Unsupported AV1 profile '%s', setting to Main profile", profile_str);
vaapi_profile = AV_PROFILE_AV1_MAIN;
}
} else {
vaapi_profile = AV_PROFILE_UNKNOWN;
blog(LOG_WARNING, "Unsupported codec '%s', setting profile to unknown", codec);
}
obs_data_set_int(encoder_settings, "profile", vaapi_profile);
}
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_canvas_get_video(canvas));
obs_video_info ovi;
if (!obs_canvas_get_video_info(canvas, &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, encoder_index);
adjust_encoder_frame_rate_divisor(ovi, video_encoder, encoder_config, encoder_index);
return video_encoder;
}
static OBSEncoderAutoRelease create_audio_encoder(const char *name, const char *audio_encoder_id, obs_data_t *settings,
size_t mixer_idx)
{
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(QWidget *parent, const QString &multitrack_video_name,
obs_data_t *dump_stream_to_file_config, const GoLiveApi::Config &go_live_config,
std::vector<OBSEncoderAutoRelease> &audio_encoders,
std::shared_ptr<obs_encoder_group_t> &video_encoder_group,
const char *audio_encoder_id, size_t main_audio_mixer,
std::optional<size_t> vod_track_mixer,
const std::vector<OBSCanvasAutoRelease> &canvases);
static void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self, obs_output_t *output, OBSSignal &start,
OBSSignal &stop);
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, size_t main_audio_mixer, std::optional<size_t> vod_track_mixer,
std::optional<bool> use_rtmps, std::optional<QString> extra_canvas)
{
{
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;
}
}
restart_on_error = false;
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, "multitrack_video_name")) {
multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name");
}
auto auto_config_url_data = auto_config_url.toUtf8();
std::vector<OBSCanvasAutoRelease> canvases;
canvases.emplace_back(obs_get_main_canvas());
if (extra_canvas) {
obs_canvas_t *canvas = obs_get_canvas_by_uuid(extra_canvas->toUtf8().constData());
if (!canvas) {
throw MultitrackVideoError::critical(QTStr("FailedToStartStream.MissingCanvas"));
}
canvases.emplace_back(canvas);
}
std::string canvasNames;
for (const auto &canvas : canvases) {
if (!canvasNames.empty())
canvasNames += ", ";
canvasNames += obs_canvas_get_name(canvas);
}
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\n"
" canvases: %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", canvasNames.c_str());
const bool custom_config_only = auto_config_url.isEmpty() && 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(), canvases);
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;
std::vector<OBSEncoderAutoRelease> audio_encoders;
std::shared_ptr<obs_encoder_group_t> video_encoder_group;
auto outputs = SetupOBSOutput(parent, multitrack_video_name, dump_stream_to_file_config, output_config,
audio_encoders, video_encoder_group, audio_encoder_id, main_audio_mixer,
vod_track_mixer, canvases);
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, use_rtmps);
if (!multitrack_video_service)
throw MultitrackVideoError::warning(
QTStr("FailedToStartStream.FallbackToDefault").arg(multitrack_video_name));
obs_output_set_service(output, multitrack_video_service);
// Register the BPM (Broadcast Performance Metrics) callback
obs_output_add_packet_callback(output, bpm_inject, NULL);
// Set callback to prevent reconnection attempts once the stream key has become invalid
static auto reconnect_cb = [](void *param, obs_output_t *, int code) -> bool {
auto _this = static_cast<MultitrackVideoOutput *>(param);
return code != OBS_OUTPUT_INVALID_STREAM || (_this->reconnect_attempts++ < MAX_RECONNECT_ATTEMPTS);
};
obs_output_set_reconnect_callback(output, reconnect_cb, this);
OBSSignal start_streaming;
OBSSignal stop_streaming;
SetupSignalHandlers(false, this, output, start_streaming, stop_streaming);
if (dump_stream_to_file_config && recording_output) {
OBSSignal start_recording;
OBSSignal stop_recording;
SetupSignalHandlers(true, this, recording_output, start_recording, stop_recording);
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));
}
std::vector<OBSCanvasAutoRelease> recording_canvases(canvases.size());
for (const auto &canvas : canvases) {
recording_canvases.emplace_back(obs_canvas_get_ref(canvas));
}
{
const std::lock_guard current_stream_dump_lock{current_stream_dump_mutex};
current_stream_dump.emplace(OBSOutputObjects{
std::move(recording_output),
video_encoder_group,
std::move(recording_audio_encoders),
nullptr,
std::move(start_recording),
std::move(stop_recording),
std::move(recording_canvases),
});
}
}
const std::lock_guard current_lock{current_mutex};
current.emplace(OBSOutputObjects{
std::move(output),
video_encoder_group,
std::move(audio_encoders),
std::move(multitrack_video_service),
std::move(start_streaming),
std::move(stop_streaming),
std::move(canvases),
});
}
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()
{
restart_on_error = false;
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 &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(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, "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) {
enableDynBitrate = false;
if (mb.clickedButton() == all_streams) {
config_set_bool(config, "Output", "DynamicBitrate", false);
}
return true;
}
MultitrackVideoOutput::ReleaseOnMainThread(take_current());
MultitrackVideoOutput::ReleaseOnMainThread(take_current_stream_dump());
return false;
}
static bool create_video_encoders(const GoLiveApi::Config &go_live_config,
std::shared_ptr<obs_encoder_group_t> &video_encoder_group, obs_output_t *output,
obs_output_t *recording_output, const std::vector<OBSCanvasAutoRelease> &canvases)
{
DStr video_encoder_name_buffer;
if (go_live_config.encoder_configurations.empty()) {
blog(LOG_WARNING, "MultitrackVideoOutput: Missing video encoder configurations");
throw MultitrackVideoError::warning(QTStr("FailedToStartStream.MissingEncoderConfigs"));
}
std::shared_ptr<obs_encoder_group_t> encoder_group(obs_encoder_group_create(), obs_encoder_group_destroy);
if (!encoder_group)
return false;
auto max_canvas_idx = canvases.size() - 1;
for (size_t i = 0; i < go_live_config.encoder_configurations.size(); i++) {
auto &config = go_live_config.encoder_configurations[i];
if (config.canvas_index > max_canvas_idx) {
blog(LOG_ERROR, "MultitrackVideoOutput: Invalid canvas index: %u", config.canvas_index);
throw MultitrackVideoError::warning(QTStr("FailedToStartStream.InvalidEncoderConfig"));
}
auto &canvas = canvases[config.canvas_index];
auto encoder = create_video_encoder(video_encoder_name_buffer, i, config, canvas);
if (!encoder)
return false;
if (!obs_encoder_set_group(encoder, encoder_group.get()))
return false;
obs_output_set_video_encoder2(output, encoder, i);
if (recording_output)
obs_output_set_video_encoder2(recording_output, encoder, i);
}
video_encoder_group = encoder_group;
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, size_t main_audio_mixer,
std::optional<size_t> vod_track_mixer, std::vector<speaker_layout> &speaker_layouts,
speaker_layout &current_layout)
{
speaker_layout speakers = SPEAKERS_UNKNOWN;
obs_audio_info oai = {};
if (obs_get_audio_info(&oai))
speakers = oai.speakers;
current_layout = speakers;
auto sanitize_audio_channels = [&](obs_encoder_t *encoder, uint32_t channels) {
speaker_layout target_speakers = SPEAKERS_UNKNOWN;
for (size_t i = 0; i <= (size_t)SPEAKERS_7POINT1; i++) {
if (get_audio_channels((speaker_layout)i) != channels)
continue;
target_speakers = (speaker_layout)i;
break;
}
if (target_speakers == SPEAKERS_UNKNOWN) {
blog(LOG_WARNING,
"MultitrackVideoOutput: Could not find "
"speaker layout for %" PRIu32 "channels "
"while configuring encoder '%s'",
channels, obs_encoder_get_name(encoder));
return;
}
if (speakers != SPEAKERS_UNKNOWN &&
(channels > get_audio_channels(speakers) || speakers == target_speakers))
return;
auto it = std::find(std::begin(speaker_layouts), std::end(speaker_layouts), target_speakers);
if (it == std::end(speaker_layouts))
speaker_layouts.push_back(target_speakers);
};
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);
OBSDataAutoRelease settings = obs_data_create_from_json(configs[i].settings.dump().c_str());
OBSEncoderAutoRelease audio_encoder =
create_audio_encoder(encoder_name_buffer->array, audio_encoder_id, settings, mixer_idx);
sanitize_audio_channels(audio_encoder, configs[i].channels);
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, main_audio_mixer);
if (!vod_track_mixer.has_value())
return;
// we already check for empty inside of `create_encoders`
encoder_configs_type empty = {};
create_encoders("multitrack video vod audio", go_live_config.audio_configurations.vod.value_or(empty),
*vod_track_mixer);
return;
}
static const char *speaker_layout_to_string(speaker_layout layout)
{
switch (layout) {
case SPEAKERS_MONO:
return "Mono";
case SPEAKERS_2POINT1:
return "2.1";
case SPEAKERS_4POINT0:
return "4.0";
case SPEAKERS_4POINT1:
return "4.1";
case SPEAKERS_5POINT1:
return "5.1";
case SPEAKERS_7POINT1:
return "7.1";
case SPEAKERS_UNKNOWN:
case SPEAKERS_STEREO:
return "Stereo";
}
return "Stereo";
}
static void handle_speaker_layout_issues(QWidget *parent, const QString &multitrack_video_name,
const std::vector<speaker_layout> &requested_layouts, speaker_layout layout)
{
if (requested_layouts.empty())
return;
QString message;
if (requested_layouts.size() == 1) {
message = QTStr("MultitrackVideo.IncompatibleSettings.AudioChannelsSingle")
.arg(QTStr(speaker_layout_to_string(requested_layouts.front())));
} else {
message =
QTStr("MultitrackVideo.IncompatibleSettings.AudioChannelsMultiple").arg(multitrack_video_name);
}
QMetaObject::invokeMethod(
parent,
[&] {
QMessageBox mb(parent);
mb.setIcon(QMessageBox::Critical);
mb.setWindowTitle(QTStr("MultitrackVideo.IncompatibleSettings.Title"));
mb.setText(QTStr("MultitrackVideo.IncompatibleSettings.AudioChannels")
.arg(multitrack_video_name)
.arg(QTStr(speaker_layout_to_string(layout)))
.arg(message));
mb.setStandardButtons(QMessageBox::StandardButton::Cancel);
mb.exec();
},
BlockingConnectionTypeFor(parent));
blog(LOG_INFO, "MultitrackVideoOutput: Attempted to start stream with incompatible "
"audio channel setting. Action taken: cancel");
throw MultitrackVideoError::cancel();
}
static OBSOutputs SetupOBSOutput(QWidget *parent, const QString &multitrack_video_name,
obs_data_t *dump_stream_to_file_config, const GoLiveApi::Config &go_live_config,
std::vector<OBSEncoderAutoRelease> &audio_encoders,
std::shared_ptr<obs_encoder_group_t> &video_encoder_group,
const char *audio_encoder_id, size_t main_audio_mixer,
std::optional<size_t> vod_track_mixer,
const std::vector<OBSCanvasAutoRelease> &canvases)
{
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_encoder_group, output, recording_output, canvases))
return {nullptr, nullptr};
std::vector<speaker_layout> requested_speaker_layouts;
speaker_layout current_layout = SPEAKERS_UNKNOWN;
create_audio_encoders(go_live_config, audio_encoders, output, recording_output, audio_encoder_id,
main_audio_mixer, vod_track_mixer, requested_speaker_layouts, current_layout);
handle_speaker_layout_issues(parent, multitrack_video_name, requested_speaker_layouts, current_layout);
return {std::move(output), std::move(recording_output)};
}
void SetupSignalHandlers(bool recording, MultitrackVideoOutput *self, obs_output_t *output, OBSSignal &start,
OBSSignal &stop)
{
auto handler = obs_output_get_signal_handler(output);
start.Connect(handler, "start", !recording ? StreamStartHandler : RecordingStartHandler, self);
stop.Connect(handler, "stop", !recording ? StreamStopHandler : RecordingStopHandler, 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 StreamStartHandler(void *arg, calldata_t *)
{
auto self = static_cast<MultitrackVideoOutput *>(arg);
self->restart_on_error = true;
self->reconnect_attempts = 0;
}
void StreamStopHandler(void *arg, calldata_t *data)
{
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);
/* Unregister the BPM (Broadcast Performance Metrics) callback and destroy the allocated metrics data. */
obs_output_remove_packet_callback(static_cast<obs_output_t *>(calldata_ptr(data, "output")), bpm_inject, NULL);
bpm_destroy(static_cast<obs_output_t *>(calldata_ptr(data, "output")));
MultitrackVideoOutput::ReleaseOnMainThread(self->take_current());
}
void RecordingStartHandler(void * /* arg */, calldata_t * /* data */)
{
blog(LOG_INFO, "MultitrackVideoOutput: recording started");
}
void RecordingStopHandler(void *arg, calldata_t *)
{
auto self = static_cast<MultitrackVideoOutput *>(arg);
blog(LOG_INFO, "MultitrackVideoOutput: recording stopped");
MultitrackVideoOutput::ReleaseOnMainThread(self->take_current_stream_dump());
}