obs-studio/UI/multitrack-video-output.cpp

1044 lines
30 KiB
C++

#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 <window-basic-main.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));
}
/* 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.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 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)
{
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());
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,
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);
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, size_t main_audio_mixer,
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,
"multitrack_video_name")) {
multitrack_video_name = obs_data_get_string(
service_settings, "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;
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);
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(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),
video_encoder_group,
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),
video_encoder_group,
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,
"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;
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;
}
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)
{
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;
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 (!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)
{
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))
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, 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());
}