Skip to content

Commit cd4d624

Browse files
Sean-DerRytoEX
authored andcommitted
obs-webrtc: Add Simulcast Support
1 parent dcdbd2e commit cd4d624

File tree

13 files changed

+364
-32
lines changed

13 files changed

+364
-32
lines changed

frontend/data/locale/en-US.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,9 @@ Basic.Settings.Stream.MultitrackVideoConfigOverride="Config Override (JSON)"
10141014
Basic.Settings.Stream.MultitrackVideoConfigOverrideEnable="Enable Config Override"
10151015
Basic.Settings.Stream.MultitrackVideoLabel="Multitrack Video"
10161016
Basic.Settings.Stream.MultitrackVideoExtraCanvas="Additional Canvas"
1017+
Basic.Settings.Stream.WHIPSimulcastLabel="Simulcast"
1018+
Basic.Settings.Stream.WHIPSimulcastInfo="Simulcast allows you to encode and send multiple video qualities. <a href='https://obsproject.com/kb/whip-streaming-guide'>Learn More</a>"
1019+
Basic.Settings.Stream.WHIPSimulcastTotalLayers="Total Layers"
10171020
Basic.Settings.Stream.AdvancedOptions="Advanced Options"
10181021

10191022
# basic mode 'output' settings

frontend/forms/OBSBasicSettings.ui

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,6 +2082,100 @@
20822082
</layout>
20832083
</widget>
20842084
</item>
2085+
<item>
2086+
<widget class="QGroupBox" name="whipSimulcastGroupBox">
2087+
<property name="title">
2088+
<string>Basic.Settings.Stream.WHIPSimulcastLabel</string>
2089+
</property>
2090+
<layout class="QVBoxLayout" name="verticalLayout_35">
2091+
<property name="leftMargin">
2092+
<number>9</number>
2093+
</property>
2094+
<property name="topMargin">
2095+
<number>2</number>
2096+
</property>
2097+
<property name="rightMargin">
2098+
<number>9</number>
2099+
</property>
2100+
<property name="bottomMargin">
2101+
<number>9</number>
2102+
</property>
2103+
<item>
2104+
<layout class="QHBoxLayout" name="horizontalLayout_33">
2105+
<item>
2106+
<spacer name="horizontalSpacer_33">
2107+
<property name="orientation">
2108+
<enum>Qt::Horizontal</enum>
2109+
</property>
2110+
<property name="sizeType">
2111+
<enum>QSizePolicy::Fixed</enum>
2112+
</property>
2113+
<property name="sizeHint" stdset="0">
2114+
<size>
2115+
<width>170</width>
2116+
<height>10</height>
2117+
</size>
2118+
</property>
2119+
</spacer>
2120+
</item>
2121+
<item>
2122+
<widget class="QLabel" name="whipSimulcastInfo">
2123+
<property name="text">
2124+
<string>Basic.Settings.Stream.WHIPSimulcastInfo</string>
2125+
</property>
2126+
<property name="textFormat">
2127+
<enum>Qt::RichText</enum>
2128+
</property>
2129+
<property name="wordWrap">
2130+
<bool>true</bool>
2131+
</property>
2132+
<property name="openExternalLinks">
2133+
<bool>true</bool>
2134+
</property>
2135+
</widget>
2136+
</item>
2137+
</layout>
2138+
</item>
2139+
<item>
2140+
<layout class="QFormLayout" name="formLayout_39">
2141+
<property name="fieldGrowthPolicy">
2142+
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
2143+
</property>
2144+
<item row="1" column="0">
2145+
<widget class="QLabel" name="whipSimulcastTotalLayersLabel">
2146+
<property name="text">
2147+
<string>Basic.Settings.Stream.WHIPSimulcastTotalLayers</string>
2148+
</property>
2149+
<property name="minimumSize">
2150+
<size>
2151+
<width>170</width>
2152+
<height>0</height>
2153+
</size>
2154+
</property>
2155+
</widget>
2156+
</item>
2157+
<item row="1" column="1">
2158+
<layout class="QHBoxLayout" name="horizontalLayout_34" stretch="0,0">
2159+
<item>
2160+
<widget class="QSpinBox" name="whipSimulcastTotalLayers">
2161+
<property name="minimum">
2162+
<number>1</number>
2163+
</property>
2164+
<property name="maximum">
2165+
<number>4</number>
2166+
</property>
2167+
<property name="value">
2168+
<number>1</number>
2169+
</property>
2170+
</widget>
2171+
</item>
2172+
</layout>
2173+
</item>
2174+
</layout>
2175+
</item>
2176+
</layout>
2177+
</widget>
2178+
</item>
20852179
<item>
20862180
<widget class="QGroupBox" name="serviceAdvancedOptionsGroupBox">
20872181
<property name="title">

frontend/settings/OBSBasicSettings.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
385385
HookWidget(ui->authUsername, EDIT_CHANGED, STREAM1_CHANGED);
386386
HookWidget(ui->authPw, EDIT_CHANGED, STREAM1_CHANGED);
387387
HookWidget(ui->ignoreRecommended, CHECK_CHANGED, STREAM1_CHANGED);
388+
HookWidget(ui->whipSimulcastTotalLayers, SCROLL_CHANGED, STREAM1_CHANGED);
388389
HookWidget(ui->enableMultitrackVideo, CHECK_CHANGED, STREAM1_CHANGED);
389390
HookWidget(ui->multitrackVideoMaximumAggregateBitrateAuto, CHECK_CHANGED, STREAM1_CHANGED);
390391
HookWidget(ui->multitrackVideoMaximumAggregateBitrate, SCROLL_CHANGED, STREAM1_CHANGED);

frontend/settings/OBSBasicSettings_Stream.cpp

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ void OBSBasicSettings::InitStreamPage()
9595
void OBSBasicSettings::LoadStream1Settings()
9696
{
9797
bool ignoreRecommended = config_get_bool(main->Config(), "Stream1", "IgnoreRecommended");
98+
int whipSimulcastTotalLayers = config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers");
9899

99100
obs_service_t *service_obj = main->GetService();
100101
const char *type = obs_service_get_type(service_obj);
@@ -209,10 +210,13 @@ void OBSBasicSettings::LoadStream1Settings()
209210
if (use_custom_server)
210211
ui->serviceCustomServer->setText(server);
211212

212-
if (is_whip)
213+
if (is_whip) {
213214
ui->key->setText(bearer_token);
214-
else
215+
ui->whipSimulcastGroupBox->show();
216+
} else {
215217
ui->key->setText(key);
218+
ui->whipSimulcastGroupBox->hide();
219+
}
216220

217221
ServiceChanged(true);
218222

@@ -226,6 +230,7 @@ void OBSBasicSettings::LoadStream1Settings()
226230
ui->streamPage->setEnabled(!streamActive);
227231

228232
ui->ignoreRecommended->setChecked(ignoreRecommended);
233+
ui->whipSimulcastTotalLayers->setValue(whipSimulcastTotalLayers);
229234

230235
loading = false;
231236

@@ -327,6 +332,9 @@ void OBSBasicSettings::SaveStream1Settings()
327332

328333
SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended");
329334

335+
auto oldWHIPSimulcastTotalLayers = config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers");
336+
SaveSpinBox(ui->whipSimulcastTotalLayers, "Stream1", "WHIPSimulcastTotalLayers");
337+
330338
auto oldMultitrackVideoSetting = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo");
331339

332340
if (!IsCustomService()) {
@@ -355,7 +363,8 @@ void OBSBasicSettings::SaveStream1Settings()
355363
SaveText(ui->multitrackVideoConfigOverride, "Stream1", "MultitrackVideoConfigOverride");
356364
SaveComboData(ui->multitrackVideoAdditionalCanvas, "Stream1", "MultitrackExtraCanvas");
357365

358-
if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked())
366+
if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked() ||
367+
oldWHIPSimulcastTotalLayers != ui->whipSimulcastTotalLayers->value())
359368
main->ResetOutputs();
360369

361370
SwapMultiTrack(QT_TO_UTF8(protocol));
@@ -588,6 +597,12 @@ void OBSBasicSettings::on_service_currentIndexChanged(int idx)
588597
} else {
589598
SwapMultiTrack(QT_TO_UTF8(protocol));
590599
}
600+
601+
if (IsWHIP()) {
602+
ui->whipSimulcastGroupBox->show();
603+
} else {
604+
ui->whipSimulcastGroupBox->hide();
605+
}
591606
}
592607

593608
void OBSBasicSettings::on_customServer_textChanged(const QString &)

frontend/utility/AdvancedOutput.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_)
132132
throw "Failed to create streaming video encoder "
133133
"(advanced output)";
134134
obs_encoder_release(videoStreaming);
135+
if (whipSimulcastEncoders != nullptr) {
136+
whipSimulcastEncoders->Create(streamEncoder, config_get_int(main->Config(), "AdvOut", "RescaleFilter"),
137+
config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers"),
138+
video_output_get_width(obs_get_video()),
139+
video_output_get_height(obs_get_video()));
140+
}
135141

136142
const char *rate_control =
137143
obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control");
@@ -247,6 +253,9 @@ void AdvancedOutput::UpdateStreamSettings()
247253
}
248254

249255
obs_encoder_update(videoStreaming, settings);
256+
if (whipSimulcastEncoders != nullptr) {
257+
whipSimulcastEncoders->Update(settings, obs_data_get_int(settings, "bitrate"));
258+
}
250259
}
251260

252261
inline void AdvancedOutput::UpdateRecordingSettings()
@@ -649,6 +658,9 @@ std::shared_future<void> AdvancedOutput::SetupStreaming(obs_service_t *service,
649658
}
650659

651660
obs_output_set_video_encoder(streamOutput, videoStreaming);
661+
if (whipSimulcastEncoders != nullptr) {
662+
whipSimulcastEncoders->SetStreamOutput(streamOutput);
663+
}
652664
obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0);
653665

654666
if (!is_multitrack_output) {

frontend/utility/BasicOutputHandler.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_)
236236

237237
if (multitrack_enabled)
238238
multitrackVideo = make_unique<MultitrackVideoOutput>();
239+
240+
if (config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers") > 1)
241+
whipSimulcastEncoders = make_unique<WHIPSimulcastEncoders>();
239242
}
240243

241244
extern void log_vcam_changed(const VCamConfig &config, bool starting);

frontend/utility/BasicOutputHandler.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <utility/MultitrackVideoOutput.hpp>
4+
#include <utility/WHIPSimulcastEncoders.hpp>
45

56
#include <obs.hpp>
67
#include <util/dstr.hpp>
@@ -42,6 +43,8 @@ struct BasicOutputHandler {
4243
obs_scene_t *vCamSourceScene = nullptr;
4344
obs_sceneitem_t *vCamSourceSceneItem = nullptr;
4445

46+
std::unique_ptr<WHIPSimulcastEncoders> whipSimulcastEncoders;
47+
4548
std::string outputType;
4649
std::string lastError;
4750

frontend/utility/SimpleOutput.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId)
7575
if (!videoStreaming)
7676
throw "Failed to create video streaming encoder (simple output)";
7777
obs_encoder_release(videoStreaming);
78+
79+
if (whipSimulcastEncoders != nullptr) {
80+
whipSimulcastEncoders->Create(encoderId, config_get_int(main->Config(), "AdvOut", "RescaleFilter"),
81+
config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers"),
82+
video_output_get_width(obs_get_video()),
83+
video_output_get_height(obs_get_video()));
84+
}
7885
}
7986

8087
/* mistakes have been made to lead us to this. */
@@ -351,11 +358,18 @@ void SimpleOutput::Update()
351358
break;
352359
default:
353360
obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12);
361+
if (whipSimulcastEncoders != nullptr) {
362+
whipSimulcastEncoders->SetVideoFormat(VIDEO_FORMAT_NV12);
363+
}
354364
}
355365

356366
obs_encoder_update(videoStreaming, videoSettings);
357367
obs_encoder_update(audioStreaming, audioSettings);
358368
obs_encoder_update(audioArchive, audioSettings);
369+
370+
if (whipSimulcastEncoders != nullptr) {
371+
whipSimulcastEncoders->Update(videoSettings, videoBitrate);
372+
}
359373
}
360374

361375
void SimpleOutput::UpdateRecordingAudioSettings()
@@ -630,6 +644,9 @@ std::shared_future<void> SimpleOutput::SetupStreaming(obs_service_t *service, Se
630644
}
631645

632646
obs_output_set_video_encoder(streamOutput, videoStreaming);
647+
if (whipSimulcastEncoders != nullptr) {
648+
whipSimulcastEncoders->SetStreamOutput(streamOutput);
649+
}
633650
obs_output_set_audio_encoder(streamOutput, audioStreaming, 0);
634651
obs_output_set_service(streamOutput, service);
635652
return true;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/******************************************************************************
2+
Copyright (C) 2025 by Sean DuBois <[email protected]>
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 2 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
******************************************************************************/
17+
#pragma once
18+
19+
struct WHIPSimulcastEncoders {
20+
public:
21+
void Create(const char *encoderId, int rescaleFilter, int whipSimulcastTotalLayers, uint32_t outputWidth,
22+
uint32_t outputHeight)
23+
{
24+
if (rescaleFilter == OBS_SCALE_DISABLE) {
25+
rescaleFilter = OBS_SCALE_BICUBIC;
26+
}
27+
28+
if (whipSimulcastTotalLayers <= 1) {
29+
return;
30+
}
31+
32+
auto widthStep = outputWidth / whipSimulcastTotalLayers;
33+
auto heightStep = outputHeight / whipSimulcastTotalLayers;
34+
std::string encoder_name = "whip_simulcast_0";
35+
36+
for (auto i = whipSimulcastTotalLayers - 1; i > 0; i--) {
37+
uint32_t width = widthStep * i;
38+
width -= width % 2;
39+
40+
uint32_t height = heightStep * i;
41+
height -= height % 2;
42+
43+
encoder_name[encoder_name.size() - 1] = std::to_string(i).at(0);
44+
auto whip_simulcast_encoder =
45+
obs_video_encoder_create(encoderId, encoder_name.c_str(), nullptr, nullptr);
46+
47+
if (whip_simulcast_encoder) {
48+
obs_encoder_set_video(whip_simulcast_encoder, obs_get_video());
49+
obs_encoder_set_scaled_size(whip_simulcast_encoder, width, height);
50+
obs_encoder_set_gpu_scale_type(whip_simulcast_encoder, (obs_scale_type)rescaleFilter);
51+
whipSimulcastEncoders.push_back(whip_simulcast_encoder);
52+
obs_encoder_release(whip_simulcast_encoder);
53+
} else {
54+
blog(LOG_WARNING,
55+
"Failed to create video streaming WHIP Simulcast encoders (BasicOutputHandler)");
56+
}
57+
}
58+
}
59+
60+
void Update(obs_data_t *videoSettings, int videoBitrate)
61+
{
62+
auto bitrateStep = videoBitrate / static_cast<int>(whipSimulcastEncoders.size() + 1);
63+
for (auto &whipSimulcastEncoder : whipSimulcastEncoders) {
64+
videoBitrate -= bitrateStep;
65+
obs_data_set_int(videoSettings, "bitrate", videoBitrate);
66+
obs_encoder_update(whipSimulcastEncoder, videoSettings);
67+
}
68+
}
69+
70+
void SetVideoFormat(enum video_format format)
71+
{
72+
for (auto enc : whipSimulcastEncoders)
73+
obs_encoder_set_preferred_video_format(enc, format);
74+
}
75+
76+
void SetStreamOutput(obs_output_t *streamOutput)
77+
{
78+
for (size_t i = 0; i < whipSimulcastEncoders.size(); i++)
79+
obs_output_set_video_encoder2(streamOutput, whipSimulcastEncoders[i], i + 1);
80+
}
81+
82+
private:
83+
std::vector<OBSEncoder> whipSimulcastEncoders;
84+
};

plugins/obs-webrtc/data/locale/en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ Service.BearerToken="Bearer Token"
44

55
Error.InvalidSDP="WHIP server responded with invalid SDP: %1"
66
Error.NoRemoteDescription="Failed to set remote description: %1"
7+
Error.SimulcastLayersRejected="WHIP server only accepted %1 simulcast layers"

0 commit comments

Comments
 (0)