diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini
index 781feb422..15b1f4aed 100644
--- a/frontend/data/locale/en-US.ini
+++ b/frontend/data/locale/en-US.ini
@@ -994,6 +994,7 @@ Basic.Settings.Stream.MultitrackVideoStreamDumpEnable="Enable stream dump to FLV
Basic.Settings.Stream.MultitrackVideoConfigOverride="Config Override (JSON)"
Basic.Settings.Stream.MultitrackVideoConfigOverrideEnable="Enable Config Override"
Basic.Settings.Stream.MultitrackVideoLabel="Multitrack Video"
+Basic.Settings.Stream.MultitrackVideoExtraCanvas="Additional Canvas"
Basic.Settings.Stream.AdvancedOptions="Advanced Options"
# basic mode 'output' settings
@@ -1576,6 +1577,7 @@ 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.MissingCanvas="A configured extra canvas is missing"
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'"
@@ -1589,6 +1591,7 @@ FailedToStartStream.WarningUnknownStatus="Received unknown status value '%1'"
FailedToStartStream.WarningRetryNonMultitrackVideo="\n
\nDo you want to continue streaming without %1?"
FailedToStartStream.WarningRetry="\n
\nDo you want to continue streaming?"
FailedToStartStream.MissingEncoderConfigs="Go live config did not include encoder configurations"
+FailedToStartStream.InvalidEncoderConfig="Go live config included an invalid encoder configuration"
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."
diff --git a/frontend/forms/OBSBasicSettings.ui b/frontend/forms/OBSBasicSettings.ui
index dfc46c0d3..812f6bb28 100644
--- a/frontend/forms/OBSBasicSettings.ui
+++ b/frontend/forms/OBSBasicSettings.ui
@@ -1907,6 +1907,29 @@
QFormLayout::AllNonFixedFieldsGrow
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 170
+ 10
+
+
+
+
+ -
+
+
+ Basic.Settings.Stream.EnableMultitrackVideo
+
+
+
-
@@ -1993,7 +2016,14 @@
- -
+
-
+
+
+ None
+
+
+
+ -
Qt::Horizontal
@@ -2009,28 +2039,28 @@
- -
+
-
Basic.Settings.Stream.MultitrackVideoStreamDumpEnable
- -
+
-
Basic.Settings.Stream.MultitrackVideoConfigOverrideEnable
- -
+
-
Basic.Settings.Stream.MultitrackVideoConfigOverride
- -
+
-
@@ -2040,29 +2070,13 @@
- -
-
+
-
+
- Basic.Settings.Stream.EnableMultitrackVideo
+ Basic.Settings.Stream.MultitrackVideoExtraCanvas
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 170
- 10
-
-
-
-
diff --git a/frontend/settings/OBSBasicSettings.cpp b/frontend/settings/OBSBasicSettings.cpp
index 5d1dbce0b..260601bd9 100644
--- a/frontend/settings/OBSBasicSettings.cpp
+++ b/frontend/settings/OBSBasicSettings.cpp
@@ -393,6 +393,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
HookWidget(ui->multitrackVideoStreamDumpEnable, CHECK_CHANGED, STREAM1_CHANGED);
HookWidget(ui->multitrackVideoConfigOverrideEnable, CHECK_CHANGED, STREAM1_CHANGED);
HookWidget(ui->multitrackVideoConfigOverride, TEXT_CHANGED, STREAM1_CHANGED);
+ HookWidget(ui->multitrackVideoAdditionalCanvas, COMBO_CHANGED, STREAM1_CHANGED);
HookWidget(ui->outputMode, COMBO_CHANGED, OUTPUTS_CHANGED);
HookWidget(ui->simpleOutputPath, EDIT_CHANGED, OUTPUTS_CHANGED);
HookWidget(ui->simpleNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED);
@@ -5621,6 +5622,7 @@ void OBSBasicSettings::UpdateMultitrackVideo()
ui->enableMultitrackVideo->isChecked());
ui->multitrackVideoMaximumVideoTracks->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked() &&
!ui->multitrackVideoMaximumVideoTracksAuto->isChecked());
+ ui->multitrackVideoAdditionalCanvas->setEnabled(toggle_available && ui->enableMultitrackVideo->isChecked());
ui->multitrackVideoStreamDumpEnable->setVisible(available && MultitrackVideoDeveloperModeEnabled());
ui->multitrackVideoConfigOverrideEnable->setVisible(available && MultitrackVideoDeveloperModeEnabled());
diff --git a/frontend/settings/OBSBasicSettings_Stream.cpp b/frontend/settings/OBSBasicSettings_Stream.cpp
index eff5d7de3..ce74a1cb1 100644
--- a/frontend/settings/OBSBasicSettings_Stream.cpp
+++ b/frontend/settings/OBSBasicSettings_Stream.cpp
@@ -170,6 +170,24 @@ void OBSBasicSettings::LoadStream1Settings()
config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride"))
.c_str());
+ ui->multitrackVideoAdditionalCanvas->clear();
+ ui->multitrackVideoAdditionalCanvas->addItem(QTStr("None"));
+ for (const auto &canvas : main->GetCanvases()) {
+ if (obs_canvas_get_flags(canvas) & EPHEMERAL)
+ continue;
+
+ ui->multitrackVideoAdditionalCanvas->addItem(obs_canvas_get_name(canvas), obs_canvas_get_uuid(canvas));
+ }
+
+ if (config_has_user_value(main->Config(), "Stream1", "MultitrackExtraCanvas")) {
+ /* Currently we only support one canvas, so the value will just be one UUID. */
+ const std::string_view uuid = config_get_string(main->Config(), "Stream1", "MultitrackExtraCanvas");
+ if (!uuid.empty()) {
+ int idx = ui->multitrackVideoAdditionalCanvas->findData(uuid.data());
+ ui->multitrackVideoAdditionalCanvas->setCurrentIndex(idx);
+ }
+ }
+
UpdateServerList();
if (is_rtmp_common) {
@@ -335,6 +353,7 @@ void OBSBasicSettings::SaveStream1Settings()
SaveCheckBox(ui->multitrackVideoStreamDumpEnable, "Stream1", "MultitrackVideoStreamDumpEnabled");
SaveCheckBox(ui->multitrackVideoConfigOverrideEnable, "Stream1", "MultitrackVideoConfigOverrideEnabled");
SaveText(ui->multitrackVideoConfigOverride, "Stream1", "MultitrackVideoConfigOverride");
+ SaveComboData(ui->multitrackVideoAdditionalCanvas, "Stream1", "MultitrackExtraCanvas");
if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked())
main->ResetOutputs();
diff --git a/frontend/utility/BasicOutputHandler.cpp b/frontend/utility/BasicOutputHandler.cpp
index 7488c4662..66ea098a1 100644
--- a/frontend/utility/BasicOutputHandler.cpp
+++ b/frontend/utility/BasicOutputHandler.cpp
@@ -457,6 +457,12 @@ std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t
custom_config = DeserializeConfigText(
config_get_string(main->Config(), "Stream1", "MultitrackVideoConfigOverride"));
+ std::optional extraCanvasUUID;
+ const char *uuid = config_get_string(main->Config(), "Stream1", "MultitrackExtraCanvas");
+ if (uuid && *uuid) {
+ extraCanvasUUID = uuid;
+ }
+
OBSDataAutoRelease settings = obs_service_get_settings(service);
QString key = obs_data_get_string(settings, "key");
@@ -532,7 +538,8 @@ std::shared_future BasicOutputHandler::SetupMultitrackVideo(obs_service_t
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,
- main_audio_mixer, vod_track_mixer, use_rtmps);
+ main_audio_mixer, vod_track_mixer, use_rtmps,
+ extraCanvasUUID);
} catch (const MultitrackVideoError &error_) {
error.emplace(error_);
}
diff --git a/frontend/utility/GoLiveAPI_PostData.cpp b/frontend/utility/GoLiveAPI_PostData.cpp
index 9d46c4033..eed63f59b 100644
--- a/frontend/utility/GoLiveAPI_PostData.cpp
+++ b/frontend/utility/GoLiveAPI_PostData.cpp
@@ -6,7 +6,8 @@
#include
GoLiveApi::PostData constructGoLivePost(QString streamKey, const std::optional &maximum_aggregate_bitrate,
- const std::optional &maximum_video_tracks, bool vod_track_enabled)
+ const std::optional &maximum_video_tracks, bool vod_track_enabled,
+ const std::vector &canvases)
{
GoLiveApi::PostData post_data{};
post_data.service = "IVS";
@@ -49,13 +50,17 @@ GoLiveApi::PostData constructGoLivePost(QString streamKey, const std::optional
GoLiveApi::PostData constructGoLivePost(QString streamKey, const std::optional &maximum_aggregate_bitrate,
- const std::optional &maximum_video_tracks, bool vod_track_enabled);
+ const std::optional &maximum_video_tracks, bool vod_track_enabled,
+ const std::vector &canvases);
diff --git a/frontend/utility/MultitrackVideoOutput.cpp b/frontend/utility/MultitrackVideoOutput.cpp
index 7eb4cfe10..0f8f5c928 100644
--- a/frontend/utility/MultitrackVideoOutput.cpp
+++ b/frontend/utility/MultitrackVideoOutput.cpp
@@ -222,7 +222,8 @@ static bool encoder_available(const char *type)
}
static OBSEncoderAutoRelease create_video_encoder(DStr &name_buffer, size_t encoder_index,
- const GoLiveApi::VideoEncoderConfiguration &encoder_config)
+ const GoLiveApi::VideoEncoderConfiguration &encoder_config,
+ const OBSCanvasAutoRelease &canvas)
{
auto encoder_type = encoder_config.type.c_str();
if (!encoder_available(encoder_type)) {
@@ -292,10 +293,10 @@ static OBSEncoderAutoRelease create_video_encoder(DStr &name_buffer, size_t enco
throw MultitrackVideoError::warning(
QTStr("FailedToStartStream.FailedToCreateVideoEncoder").arg(name_buffer->array, encoder_type));
}
- obs_encoder_set_video(video_encoder, obs_get_video());
+ obs_encoder_set_video(video_encoder, obs_canvas_get_video(canvas));
obs_video_info ovi;
- if (!obs_get_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));
@@ -329,18 +330,17 @@ static OBSOutputs SetupOBSOutput(QWidget *parent, const QString &multitrack_vide
std::vector &audio_encoders,
std::shared_ptr &video_encoder_group,
const char *audio_encoder_id, size_t main_audio_mixer,
- std::optional vod_track_mixer);
+ std::optional vod_track_mixer,
+ const std::vector &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 &rtmp_url, const QString &stream_key,
- const char *audio_encoder_id,
- std::optional maximum_aggregate_bitrate,
- std::optional maximum_video_tracks,
- std::optional custom_config,
- obs_data_t *dump_stream_to_file_config, size_t main_audio_mixer,
- std::optional vod_track_mixer, std::optional use_rtmps)
+void MultitrackVideoOutput::PrepareStreaming(
+ QWidget *parent, const char *service_name, obs_service_t *service, const std::optional &rtmp_url,
+ const QString &stream_key, const char *audio_encoder_id, std::optional maximum_aggregate_bitrate,
+ std::optional maximum_video_tracks, std::optional custom_config,
+ obs_data_t *dump_stream_to_file_config, size_t main_audio_mixer, std::optional vod_track_mixer,
+ std::optional use_rtmps, std::optional extra_canvas)
{
{
const std::lock_guard current_lock{current_mutex};
@@ -366,6 +366,25 @@ void MultitrackVideoOutput::PrepareStreaming(QWidget *parent, const char *servic
auto auto_config_url_data = auto_config_url.toUtf8();
+ std::vector 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());
@@ -379,13 +398,14 @@ void MultitrackVideoOutput::PrepareStreaming(QWidget *parent, const char *servic
" max aggregate bitrate: %s (%" PRIu32 ")\n"
" max video tracks: %s (%" PRIu32 ")\n"
" custom rtmp url: %s ('%s')\n"
- " vod track: %s",
+ " 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");
+ vod_track_info_storage->array ? vod_track_info_storage->array : "No", canvasNames.c_str());
const bool custom_config_only = auto_config_url.isEmpty() && MultitrackVideoDeveloperModeEnabled() &&
custom_config.has_value() &&
@@ -393,7 +413,7 @@ void MultitrackVideoOutput::PrepareStreaming(QWidget *parent, const char *servic
if (!custom_config_only) {
auto go_live_post = constructGoLivePost(stream_key, maximum_aggregate_bitrate, maximum_video_tracks,
- vod_track_mixer.has_value());
+ vod_track_mixer.has_value(), canvases);
go_live_config = DownloadGoLiveConfig(parent, auto_config_url, go_live_post, multitrack_video_name);
}
@@ -436,7 +456,7 @@ void MultitrackVideoOutput::PrepareStreaming(QWidget *parent, const char *servic
std::shared_ptr 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);
+ vod_track_mixer, canvases);
auto output = std::move(outputs.output);
auto recording_output = std::move(outputs.recording_output);
if (!output)
@@ -474,6 +494,11 @@ void MultitrackVideoOutput::PrepareStreaming(QWidget *parent, const char *servic
recording_audio_encoders.emplace_back(obs_encoder_get_ref(encoder));
}
+ std::vector 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{
@@ -483,6 +508,7 @@ void MultitrackVideoOutput::PrepareStreaming(QWidget *parent, const char *servic
nullptr,
std::move(start_recording),
std::move(stop_recording),
+ std::move(recording_canvases),
});
}
}
@@ -495,6 +521,7 @@ void MultitrackVideoOutput::PrepareStreaming(QWidget *parent, const char *servic
std::move(multitrack_video_service),
std::move(start_streaming),
std::move(stop_streaming),
+ std::move(canvases),
});
}
@@ -622,7 +649,7 @@ bool MultitrackVideoOutput::HandleIncompatibleSettings(QWidget *parent, config_t
static bool create_video_encoders(const GoLiveApi::Config &go_live_config,
std::shared_ptr &video_encoder_group, obs_output_t *output,
- obs_output_t *recording_output)
+ obs_output_t *recording_output, const std::vector &canvases)
{
DStr video_encoder_name_buffer;
if (go_live_config.encoder_configurations.empty()) {
@@ -634,9 +661,17 @@ static bool create_video_encoders(const GoLiveApi::Config &go_live_config,
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 encoder =
- create_video_encoder(video_encoder_name_buffer, i, go_live_config.encoder_configurations[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;
@@ -797,14 +832,15 @@ static OBSOutputs SetupOBSOutput(QWidget *parent, const QString &multitrack_vide
std::vector &audio_encoders,
std::shared_ptr &video_encoder_group,
const char *audio_encoder_id, size_t main_audio_mixer,
- std::optional vod_track_mixer)
+ std::optional vod_track_mixer,
+ const std::vector &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))
+ if (!create_video_encoders(go_live_config, video_encoder_group, output, recording_output, canvases))
return {nullptr, nullptr};
std::vector requested_speaker_layouts;
diff --git a/frontend/utility/MultitrackVideoOutput.hpp b/frontend/utility/MultitrackVideoOutput.hpp
index e5e6433a0..f66d14c03 100644
--- a/frontend/utility/MultitrackVideoOutput.hpp
+++ b/frontend/utility/MultitrackVideoOutput.hpp
@@ -27,7 +27,8 @@ public:
const char *audio_encoder_id, std::optional maximum_aggregate_bitrate,
std::optional maximum_video_tracks, std::optional custom_config,
obs_data_t *dump_stream_to_file_config, size_t main_audio_mixer,
- std::optional vod_track_mixer, std::optional use_rtmps);
+ std::optional vod_track_mixer, std::optional use_rtmps,
+ std::optional extra_canvas);
signal_handler_t *StreamingSignalHandler();
void StartedStreaming();
void StopStreaming();
@@ -49,6 +50,7 @@ private:
std::vector audio_encoders_;
OBSServiceAutoRelease multitrack_video_service_;
OBSSignal start_signal, stop_signal;
+ std::vector canvases;
};
std::optional take_current();
diff --git a/frontend/wizards/AutoConfigStreamPage.cpp b/frontend/wizards/AutoConfigStreamPage.cpp
index 86d11c3cf..3086185df 100644
--- a/frontend/wizards/AutoConfigStreamPage.cpp
+++ b/frontend/wizards/AutoConfigStreamPage.cpp
@@ -173,8 +173,10 @@ bool AutoConfigStreamPage::validatePage()
wiz->testMultitrackVideo = ui->useMultitrackVideo->isChecked();
if (wiz->testMultitrackVideo) {
+ std::vector canvases;
+ canvases.emplace_back(obs_get_main_canvas());
auto postData = constructGoLivePost(QString::fromStdString(wiz->key), std::nullopt,
- std::nullopt, false);
+ std::nullopt, false, canvases);
OBSDataAutoRelease service_settings = obs_service_get_settings(service);
auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel");