diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini
index b3303340cbe028..85a2c84f761567 100644
--- a/UI/data/locale/en-US.ini
+++ b/UI/data/locale/en-US.ini
@@ -956,6 +956,7 @@ Basic.Settings.Stream.StreamSettingsWarning="Open Settings"
Basic.Settings.Stream.MissingUrlAndApiKey="URL and Stream Key are missing.\n\nOpen settings to enter the URL and Stream Key in the 'stream' tab."
Basic.Settings.Stream.MissingUrl="Stream URL is missing.\n\nOpen settings to enter the URL in the 'Stream' tab."
Basic.Settings.Stream.MissingStreamKey="Stream key is missing.\n\nOpen settings to enter the stream key in the 'Stream' tab."
+Basic.Settings.Stream.UseSimulcast="Use Simulcast"
Basic.Settings.Stream.IgnoreRecommended="Ignore streaming service setting recommendations"
Basic.Settings.Stream.IgnoreRecommended.Warn.Title="Override Recommended Settings"
Basic.Settings.Stream.IgnoreRecommended.Warn.Text="Warning: Ignoring the service's limitations may result in degraded stream quality or prevent you from streaming.\n\nContinue?"
diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui
index c58271dd156332..66dbc3ff587639 100644
--- a/UI/forms/OBSBasicSettings.ui
+++ b/UI/forms/OBSBasicSettings.ui
@@ -1926,6 +1926,13 @@
+ -
+
+
+ Basic.Settings.Stream.UseSimulcast
+
+
+
diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp
index 9e644e90acb8df..6ae4eb9583990b 100644
--- a/UI/window-basic-main-outputs.cpp
+++ b/UI/window-basic-main-outputs.cpp
@@ -588,6 +588,10 @@ void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId)
if (!videoStreaming)
throw "Failed to create video streaming encoder (simple output)";
obs_encoder_release(videoStreaming);
+
+ if (config_get_bool(main->Config(), "Stream1", "UseSimulcast")) {
+ CreateSimulcastEncoders(encoderId);
+ }
}
/* mistakes have been made to lead us to this. */
@@ -890,9 +894,14 @@ void SimpleOutput::Update()
default:
obs_encoder_set_preferred_video_format(videoStreaming,
VIDEO_FORMAT_NV12);
+ for (auto enc : simulcastEncoders)
+ obs_encoder_set_preferred_video_format(
+ enc, VIDEO_FORMAT_NV12);
}
obs_encoder_update(videoStreaming, videoSettings);
+ SimulcastEncodersUpdate(videoSettings, videoBitrate);
+
obs_encoder_update(audioStreaming, audioSettings);
obs_encoder_update(audioArchive, audioSettings);
}
@@ -1204,6 +1213,9 @@ SimpleOutput::SetupStreaming(obs_service_t *service,
}
obs_output_set_video_encoder(streamOutput, videoStreaming);
+ for (size_t i = 0; i < simulcastEncoders.size(); i++)
+ obs_output_set_video_encoder2(
+ streamOutput, simulcastEncoders[i], i + 1);
obs_output_set_audio_encoder(streamOutput, audioStreaming, 0);
obs_output_set_service(streamOutput, service);
return true;
@@ -1747,6 +1759,10 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_)
"(advanced output)";
obs_encoder_release(videoStreaming);
+ if (config_get_bool(main->Config(), "Stream1", "UseSimulcast")) {
+ CreateSimulcastEncoders(streamEncoder);
+ }
+
const char *rate_control = obs_data_get_string(
useStreamEncoder ? streamEncSettings : recordEncSettings,
"rate_control");
@@ -1867,6 +1883,8 @@ void AdvancedOutput::UpdateStreamSettings()
}
obs_encoder_update(videoStreaming, settings);
+ SimulcastEncodersUpdate(settings,
+ obs_data_get_int(settings, "bitrate"));
}
inline void AdvancedOutput::UpdateRecordingSettings()
@@ -2351,6 +2369,9 @@ AdvancedOutput::SetupStreaming(obs_service_t *service,
}
obs_output_set_video_encoder(streamOutput, videoStreaming);
+ for (size_t i = 0; i < simulcastEncoders.size(); i++)
+ obs_output_set_video_encoder2(
+ streamOutput, simulcastEncoders[i], i + 1);
obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0);
if (!is_multitrack_output) {
@@ -2900,3 +2921,52 @@ BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main)
{
return new AdvancedOutput(main);
}
+
+void BasicOutputHandler::CreateSimulcastEncoders(const char *encoderId)
+{
+ int rescaleFilter =
+ config_get_int(main->Config(), "AdvOut", "RescaleFilter");
+ if (rescaleFilter == OBS_SCALE_DISABLE) {
+ rescaleFilter = OBS_SCALE_BICUBIC;
+ }
+
+ std::string encoder_name = "simulcast_0";
+ for (auto i = 0; i < 2; i++) {
+ uint32_t width = video_output_get_width(obs_get_video()) /
+ (1.5 + (.5 * i));
+ width -= width % 2;
+
+ uint32_t height = video_output_get_height(obs_get_video()) /
+ (1.5 + (.5 * i));
+ height -= height % 2;
+
+ encoder_name[encoder_name.size() - 1] = to_string(i).at(0);
+ auto simulcast_encoder = obs_video_encoder_create(
+ encoderId, encoder_name.c_str(), nullptr, nullptr);
+
+ if (simulcast_encoder) {
+ obs_encoder_set_video(simulcast_encoder,
+ obs_get_video());
+ obs_encoder_set_scaled_size(simulcast_encoder, width,
+ height);
+ obs_encoder_set_gpu_scale_type(
+ simulcast_encoder,
+ (obs_scale_type)rescaleFilter);
+ simulcastEncoders.push_back(simulcast_encoder);
+ obs_encoder_release(simulcast_encoder);
+ } else {
+ blog(LOG_WARNING,
+ "Failed to create video streaming simulcast encoders (BasicOutputHandler)");
+ }
+ }
+}
+
+void BasicOutputHandler::SimulcastEncodersUpdate(obs_data_t *videoSettings,
+ int videoBitrate)
+{
+ for (size_t i = 0; i < simulcastEncoders.size(); i++) {
+ obs_data_set_int(videoSettings, "bitrate",
+ videoBitrate / (2 * (i + 1)));
+ obs_encoder_update(simulcastEncoders[i], videoSettings);
+ }
+}
diff --git a/UI/window-basic-main-outputs.hpp b/UI/window-basic-main-outputs.hpp
index 870f720a225ae3..d2ad9102997c33 100644
--- a/UI/window-basic-main-outputs.hpp
+++ b/UI/window-basic-main-outputs.hpp
@@ -38,6 +38,8 @@ struct BasicOutputHandler {
obs_scene_t *vCamSourceScene = nullptr;
obs_sceneitem_t *vCamSourceSceneItem = nullptr;
+ std::vector simulcastEncoders;
+
std::string outputType;
std::string lastError;
@@ -105,6 +107,9 @@ struct BasicOutputHandler {
size_t main_audio_mixer, std::optional vod_track_mixer,
std::function)> continuation);
OBSDataAutoRelease GenerateMultitrackVideoStreamDumpConfig();
+ void CreateSimulcastEncoders(const char *encoderId);
+ void SimulcastEncodersUpdate(obs_data_t *videoSettings,
+ int videoBitrate);
};
BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main);
diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp
index 881e1d54247f53..b9de8622b24c6d 100644
--- a/UI/window-basic-settings-stream.cpp
+++ b/UI/window-basic-settings-stream.cpp
@@ -112,6 +112,8 @@ void OBSBasicSettings::LoadStream1Settings()
{
bool ignoreRecommended =
config_get_bool(main->Config(), "Stream1", "IgnoreRecommended");
+ bool useSimulcast =
+ config_get_bool(main->Config(), "Stream1", "UseSimulcast");
obs_service_t *service_obj = main->GetService();
const char *type = obs_service_get_type(service_obj);
@@ -224,10 +226,13 @@ void OBSBasicSettings::LoadStream1Settings()
if (use_custom_server)
ui->serviceCustomServer->setText(server);
- if (is_whip)
+ if (is_whip) {
ui->key->setText(bearer_token);
- else
+ ui->useSimulcast->show();
+ } else {
ui->key->setText(key);
+ ui->useSimulcast->hide();
+ }
ServiceChanged(true);
@@ -241,6 +246,7 @@ void OBSBasicSettings::LoadStream1Settings()
ui->streamPage->setEnabled(!streamActive);
ui->ignoreRecommended->setChecked(ignoreRecommended);
+ ui->useSimulcast->setChecked(useSimulcast);
loading = false;
@@ -365,6 +371,10 @@ void OBSBasicSettings::SaveStream1Settings()
SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended");
+ auto oldSimulcastSetting =
+ config_get_bool(main->Config(), "Stream1", "UseSimulcast");
+ SaveCheckBox(ui->useSimulcast, "Stream1", "UseSimulcast");
+
auto oldMultitrackVideoSetting = config_get_bool(
main->Config(), "Stream1", "EnableMultitrackVideo");
@@ -404,7 +414,9 @@ void OBSBasicSettings::SaveStream1Settings()
SaveText(ui->multitrackVideoConfigOverride, "Stream1",
"MultitrackVideoConfigOverride");
- if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked())
+ if (oldMultitrackVideoSetting !=
+ ui->enableMultitrackVideo->isChecked() ||
+ oldSimulcastSetting != ui->useSimulcast->isChecked())
main->ResetOutputs();
SwapMultiTrack(QT_TO_UTF8(protocol));
@@ -661,6 +673,12 @@ void OBSBasicSettings::on_service_currentIndexChanged(int idx)
} else {
SwapMultiTrack(QT_TO_UTF8(protocol));
}
+
+ if (IsWHIP()) {
+ ui->useSimulcast->show();
+ } else {
+ ui->useSimulcast->hide();
+ }
}
void OBSBasicSettings::on_customServer_textChanged(const QString &)
diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp
index dfcd2dd653ca91..73d00e211e1beb 100644
--- a/UI/window-basic-settings.cpp
+++ b/UI/window-basic-settings.cpp
@@ -420,6 +420,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
HookWidget(ui->authUsername, EDIT_CHANGED, STREAM1_CHANGED);
HookWidget(ui->authPw, EDIT_CHANGED, STREAM1_CHANGED);
HookWidget(ui->ignoreRecommended, CHECK_CHANGED, STREAM1_CHANGED);
+ HookWidget(ui->useSimulcast, CHECK_CHANGED, STREAM1_CHANGED);
HookWidget(ui->enableMultitrackVideo, CHECK_CHANGED, STREAM1_CHANGED);
HookWidget(ui->multitrackVideoMaximumAggregateBitrateAuto, CHECK_CHANGED, STREAM1_CHANGED);
HookWidget(ui->multitrackVideoMaximumAggregateBitrate, SCROLL_CHANGED, STREAM1_CHANGED);
diff --git a/plugins/obs-webrtc/whip-output.cpp b/plugins/obs-webrtc/whip-output.cpp
index 8786fc16959589..440ee982bf1032 100644
--- a/plugins/obs-webrtc/whip-output.cpp
+++ b/plugins/obs-webrtc/whip-output.cpp
@@ -24,6 +24,14 @@ const uint8_t video_payload_type = 96;
// ~3 seconds of 8.5 Megabit video
const int video_nack_buffer_size = 4000;
+const std::string rtpHeaderExtUriMid = "urn:ietf:params:rtp-hdrext:sdes:mid";
+const std::string rtpHeaderExtUriRid =
+ "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id";
+
+const std::string highRid = "h";
+const std::string medRid = "m";
+const std::string lowRid = "l";
+
WHIPOutput::WHIPOutput(obs_data_t *, obs_output_t *output)
: output(output),
endpoint_url(),
@@ -39,8 +47,7 @@ WHIPOutput::WHIPOutput(obs_data_t *, obs_output_t *output)
total_bytes_sent(0),
connect_time_ms(0),
start_time_ns(0),
- last_audio_timestamp(0),
- last_video_timestamp(0)
+ last_audio_timestamp(0)
{
}
@@ -57,6 +64,27 @@ bool WHIPOutput::Start()
{
std::lock_guard l(start_stop_mutex);
+ for (size_t idx = 0; idx < 5; idx++) {
+ auto encoder = obs_output_get_video_encoder2(output, idx);
+ if (encoder == nullptr) {
+ break;
+ }
+
+ auto v = std::make_shared();
+ if (idx == 0) {
+ v->ssrc = base_ssrc + 1;
+ v->rid = highRid;
+ } else if (idx == 1) {
+ v->ssrc = base_ssrc + 2;
+ v->rid = medRid;
+ } else if (idx == 2) {
+ v->ssrc = base_ssrc + 3;
+ v->rid = lowRid;
+ }
+
+ videoLayerStates[obs_encoder_get_width(encoder)] = v;
+ }
+
if (!obs_output_can_begin_data_capture(output, 0))
return false;
if (!obs_output_initialize_encoders(output, 0))
@@ -92,10 +120,28 @@ void WHIPOutput::Data(struct encoder_packet *packet)
audio_sr_reporter);
last_audio_timestamp = packet->dts_usec;
} else if (video_track && packet->type == OBS_ENCODER_VIDEO) {
- int64_t duration = packet->dts_usec - last_video_timestamp;
+ auto rtp_config = video_sr_reporter->rtpConfig;
+ auto videoLayerState =
+ videoLayerStates[obs_encoder_get_width(packet->encoder)];
+ if (videoLayerState == nullptr) {
+ Stop(false);
+ obs_output_signal_stop(output, OBS_OUTPUT_ENCODE_ERROR);
+ return;
+ }
+
+ rtp_config->sequenceNumber = videoLayerState->sequenceNumber;
+ rtp_config->ssrc = videoLayerState->ssrc;
+ rtp_config->rid = videoLayerState->rid;
+ rtp_config->timestamp = videoLayerState->rtpTimestamp;
+ int64_t duration =
+ packet->dts_usec - videoLayerState->lastVideoTimestamp;
+
Send(packet->data, packet->size, duration, video_track,
video_sr_reporter);
- last_video_timestamp = packet->dts_usec;
+
+ videoLayerState->sequenceNumber = rtp_config->sequenceNumber;
+ videoLayerState->lastVideoTimestamp = packet->dts_usec;
+ videoLayerState->rtpTimestamp = rtp_config->timestamp;
}
}
@@ -151,9 +197,24 @@ void WHIPOutput::ConfigureVideoTrack(std::string media_stream_id,
video_description.addSSRC(ssrc, cname, media_stream_id,
media_stream_track_id);
+ video_description.addExtMap(
+ rtc::Description::Entry::ExtMap(1, rtpHeaderExtUriMid));
+ video_description.addExtMap(
+ rtc::Description::Entry::ExtMap(2, rtpHeaderExtUriRid));
+
+ if (videoLayerStates.size() >= 2) {
+ for (auto i = videoLayerStates.rbegin();
+ i != videoLayerStates.rend(); i++) {
+ video_description.addRid(i->second->rid);
+ }
+ }
+
auto rtp_config = std::make_shared(
ssrc, cname, video_payload_type,
rtc::H264RtpPacketizer::defaultClockRate);
+ rtp_config->midId = 1;
+ rtp_config->ridId = 2;
+ rtp_config->mid = video_mid;
const obs_encoder_t *encoder = obs_output_get_video_encoder2(output, 0);
if (!encoder)
@@ -653,7 +714,7 @@ void WHIPOutput::StopThread(bool signal)
connect_time_ms = 0;
start_time_ns = 0;
last_audio_timestamp = 0;
- last_video_timestamp = 0;
+ videoLayerStates.clear();
}
void WHIPOutput::Send(void *data, uintptr_t size, uint64_t duration,
@@ -697,7 +758,8 @@ void WHIPOutput::Send(void *data, uintptr_t size, uint64_t duration,
void register_whip_output()
{
- const uint32_t base_flags = OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE;
+ const uint32_t base_flags = OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE |
+ OBS_OUTPUT_MULTI_TRACK_AV;
const char *audio_codecs = "opus";
#ifdef ENABLE_HEVC
diff --git a/plugins/obs-webrtc/whip-output.h b/plugins/obs-webrtc/whip-output.h
index 88fd433eefc249..6eda16f19f3b85 100644
--- a/plugins/obs-webrtc/whip-output.h
+++ b/plugins/obs-webrtc/whip-output.h
@@ -13,6 +13,14 @@
#include