Skip to content

Commit 03eb4a5

Browse files
committed
obs-webrtc: Add Simulcast Support
1 parent ec5297b commit 03eb4a5

File tree

8 files changed

+183
-10
lines changed

8 files changed

+183
-10
lines changed

UI/data/locale/en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,7 @@ Basic.Settings.Stream.StreamSettingsWarning="Open Settings"
956956
Basic.Settings.Stream.MissingUrlAndApiKey="URL and Stream Key are missing.\n\nOpen settings to enter the URL and Stream Key in the 'stream' tab."
957957
Basic.Settings.Stream.MissingUrl="Stream URL is missing.\n\nOpen settings to enter the URL in the 'Stream' tab."
958958
Basic.Settings.Stream.MissingStreamKey="Stream key is missing.\n\nOpen settings to enter the stream key in the 'Stream' tab."
959+
Basic.Settings.Stream.UseSimulcast="Use Simulcast"
959960
Basic.Settings.Stream.IgnoreRecommended="Ignore streaming service setting recommendations"
960961
Basic.Settings.Stream.IgnoreRecommended.Warn.Title="Override Recommended Settings"
961962
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?"

UI/forms/OBSBasicSettings.ui

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,13 @@
19261926
</property>
19271927
</widget>
19281928
</item>
1929+
<item row="4" column="1">
1930+
<widget class="QCheckBox" name="useSimulcast">
1931+
<property name="text">
1932+
<string>Basic.Settings.Stream.UseSimulcast</string>
1933+
</property>
1934+
</widget>
1935+
</item>
19291936
</layout>
19301937
</widget>
19311938
</item>

UI/window-basic-main-outputs.cpp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,10 @@ void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId)
588588
if (!videoStreaming)
589589
throw "Failed to create video streaming encoder (simple output)";
590590
obs_encoder_release(videoStreaming);
591+
592+
if (config_get_bool(main->Config(), "Stream1", "UseSimulcast")) {
593+
CreateSimulcastEncoders(encoderId);
594+
}
591595
}
592596

593597
/* mistakes have been made to lead us to this. */
@@ -890,9 +894,14 @@ void SimpleOutput::Update()
890894
default:
891895
obs_encoder_set_preferred_video_format(videoStreaming,
892896
VIDEO_FORMAT_NV12);
897+
for (auto enc : simulcastEncoders)
898+
obs_encoder_set_preferred_video_format(
899+
enc, VIDEO_FORMAT_NV12);
893900
}
894901

895902
obs_encoder_update(videoStreaming, videoSettings);
903+
SimulcastEncodersUpdate(videoSettings, videoBitrate);
904+
896905
obs_encoder_update(audioStreaming, audioSettings);
897906
obs_encoder_update(audioArchive, audioSettings);
898907
}
@@ -1204,6 +1213,9 @@ SimpleOutput::SetupStreaming(obs_service_t *service,
12041213
}
12051214

12061215
obs_output_set_video_encoder(streamOutput, videoStreaming);
1216+
for (size_t i = 0; i < simulcastEncoders.size(); i++)
1217+
obs_output_set_video_encoder2(
1218+
streamOutput, simulcastEncoders[i], i + 1);
12071219
obs_output_set_audio_encoder(streamOutput, audioStreaming, 0);
12081220
obs_output_set_service(streamOutput, service);
12091221
return true;
@@ -1747,6 +1759,10 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_)
17471759
"(advanced output)";
17481760
obs_encoder_release(videoStreaming);
17491761

1762+
if (config_get_bool(main->Config(), "Stream1", "UseSimulcast")) {
1763+
CreateSimulcastEncoders(streamEncoder);
1764+
}
1765+
17501766
const char *rate_control = obs_data_get_string(
17511767
useStreamEncoder ? streamEncSettings : recordEncSettings,
17521768
"rate_control");
@@ -1867,6 +1883,8 @@ void AdvancedOutput::UpdateStreamSettings()
18671883
}
18681884

18691885
obs_encoder_update(videoStreaming, settings);
1886+
SimulcastEncodersUpdate(settings,
1887+
obs_data_get_int(settings, "bitrate"));
18701888
}
18711889

18721890
inline void AdvancedOutput::UpdateRecordingSettings()
@@ -2351,6 +2369,9 @@ AdvancedOutput::SetupStreaming(obs_service_t *service,
23512369
}
23522370

23532371
obs_output_set_video_encoder(streamOutput, videoStreaming);
2372+
for (size_t i = 0; i < simulcastEncoders.size(); i++)
2373+
obs_output_set_video_encoder2(
2374+
streamOutput, simulcastEncoders[i], i + 1);
23542375
obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0);
23552376

23562377
if (!is_multitrack_output) {
@@ -2900,3 +2921,52 @@ BasicOutputHandler *CreateAdvancedOutputHandler(OBSBasic *main)
29002921
{
29012922
return new AdvancedOutput(main);
29022923
}
2924+
2925+
void BasicOutputHandler::CreateSimulcastEncoders(const char *encoderId)
2926+
{
2927+
int rescaleFilter =
2928+
config_get_int(main->Config(), "AdvOut", "RescaleFilter");
2929+
if (rescaleFilter == OBS_SCALE_DISABLE) {
2930+
rescaleFilter = OBS_SCALE_BICUBIC;
2931+
}
2932+
2933+
std::string encoder_name = "simulcast_0";
2934+
for (auto i = 0; i < 2; i++) {
2935+
uint32_t width = video_output_get_width(obs_get_video()) /
2936+
(1.5 + (.5 * i));
2937+
width -= width % 2;
2938+
2939+
uint32_t height = video_output_get_height(obs_get_video()) /
2940+
(1.5 + (.5 * i));
2941+
height -= height % 2;
2942+
2943+
encoder_name[encoder_name.size() - 1] = to_string(i).at(0);
2944+
auto simulcast_encoder = obs_video_encoder_create(
2945+
encoderId, encoder_name.c_str(), nullptr, nullptr);
2946+
2947+
if (simulcast_encoder) {
2948+
obs_encoder_set_video(simulcast_encoder,
2949+
obs_get_video());
2950+
obs_encoder_set_scaled_size(simulcast_encoder, width,
2951+
height);
2952+
obs_encoder_set_gpu_scale_type(
2953+
simulcast_encoder,
2954+
(obs_scale_type)rescaleFilter);
2955+
simulcastEncoders.push_back(simulcast_encoder);
2956+
obs_encoder_release(simulcast_encoder);
2957+
} else {
2958+
blog(LOG_WARNING,
2959+
"Failed to create video streaming simulcast encoders (BasicOutputHandler)");
2960+
}
2961+
}
2962+
}
2963+
2964+
void BasicOutputHandler::SimulcastEncodersUpdate(obs_data_t *videoSettings,
2965+
int videoBitrate)
2966+
{
2967+
for (size_t i = 0; i < simulcastEncoders.size(); i++) {
2968+
obs_data_set_int(videoSettings, "bitrate",
2969+
videoBitrate / (2 * (i + 1)));
2970+
obs_encoder_update(simulcastEncoders[i], videoSettings);
2971+
}
2972+
}

UI/window-basic-main-outputs.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ struct BasicOutputHandler {
3838
obs_scene_t *vCamSourceScene = nullptr;
3939
obs_sceneitem_t *vCamSourceSceneItem = nullptr;
4040

41+
std::vector<OBSEncoder> simulcastEncoders;
42+
4143
std::string outputType;
4244
std::string lastError;
4345

@@ -105,6 +107,9 @@ struct BasicOutputHandler {
105107
size_t main_audio_mixer, std::optional<size_t> vod_track_mixer,
106108
std::function<void(std::optional<bool>)> continuation);
107109
OBSDataAutoRelease GenerateMultitrackVideoStreamDumpConfig();
110+
void CreateSimulcastEncoders(const char *encoderId);
111+
void SimulcastEncodersUpdate(obs_data_t *videoSettings,
112+
int videoBitrate);
108113
};
109114

110115
BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main);

UI/window-basic-settings-stream.cpp

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ void OBSBasicSettings::LoadStream1Settings()
112112
{
113113
bool ignoreRecommended =
114114
config_get_bool(main->Config(), "Stream1", "IgnoreRecommended");
115+
bool useSimulcast =
116+
config_get_bool(main->Config(), "Stream1", "UseSimulcast");
115117

116118
obs_service_t *service_obj = main->GetService();
117119
const char *type = obs_service_get_type(service_obj);
@@ -224,10 +226,13 @@ void OBSBasicSettings::LoadStream1Settings()
224226
if (use_custom_server)
225227
ui->serviceCustomServer->setText(server);
226228

227-
if (is_whip)
229+
if (is_whip) {
228230
ui->key->setText(bearer_token);
229-
else
231+
ui->useSimulcast->show();
232+
} else {
230233
ui->key->setText(key);
234+
ui->useSimulcast->hide();
235+
}
231236

232237
ServiceChanged(true);
233238

@@ -241,6 +246,7 @@ void OBSBasicSettings::LoadStream1Settings()
241246
ui->streamPage->setEnabled(!streamActive);
242247

243248
ui->ignoreRecommended->setChecked(ignoreRecommended);
249+
ui->useSimulcast->setChecked(useSimulcast);
244250

245251
loading = false;
246252

@@ -365,6 +371,10 @@ void OBSBasicSettings::SaveStream1Settings()
365371

366372
SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended");
367373

374+
auto oldSimulcastSetting =
375+
config_get_bool(main->Config(), "Stream1", "UseSimulcast");
376+
SaveCheckBox(ui->useSimulcast, "Stream1", "UseSimulcast");
377+
368378
auto oldMultitrackVideoSetting = config_get_bool(
369379
main->Config(), "Stream1", "EnableMultitrackVideo");
370380

@@ -404,7 +414,9 @@ void OBSBasicSettings::SaveStream1Settings()
404414
SaveText(ui->multitrackVideoConfigOverride, "Stream1",
405415
"MultitrackVideoConfigOverride");
406416

407-
if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked())
417+
if (oldMultitrackVideoSetting !=
418+
ui->enableMultitrackVideo->isChecked() ||
419+
oldSimulcastSetting != ui->useSimulcast->isChecked())
408420
main->ResetOutputs();
409421

410422
SwapMultiTrack(QT_TO_UTF8(protocol));
@@ -661,6 +673,12 @@ void OBSBasicSettings::on_service_currentIndexChanged(int idx)
661673
} else {
662674
SwapMultiTrack(QT_TO_UTF8(protocol));
663675
}
676+
677+
if (IsWHIP()) {
678+
ui->useSimulcast->show();
679+
} else {
680+
ui->useSimulcast->hide();
681+
}
664682
}
665683

666684
void OBSBasicSettings::on_customServer_textChanged(const QString &)

UI/window-basic-settings.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
420420
HookWidget(ui->authUsername, EDIT_CHANGED, STREAM1_CHANGED);
421421
HookWidget(ui->authPw, EDIT_CHANGED, STREAM1_CHANGED);
422422
HookWidget(ui->ignoreRecommended, CHECK_CHANGED, STREAM1_CHANGED);
423+
HookWidget(ui->useSimulcast, CHECK_CHANGED, STREAM1_CHANGED);
423424
HookWidget(ui->enableMultitrackVideo, CHECK_CHANGED, STREAM1_CHANGED);
424425
HookWidget(ui->multitrackVideoMaximumAggregateBitrateAuto, CHECK_CHANGED, STREAM1_CHANGED);
425426
HookWidget(ui->multitrackVideoMaximumAggregateBitrate, SCROLL_CHANGED, STREAM1_CHANGED);

plugins/obs-webrtc/whip-output.cpp

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ const uint8_t video_payload_type = 96;
2424
// ~3 seconds of 8.5 Megabit video
2525
const int video_nack_buffer_size = 4000;
2626

27+
const std::string rtpHeaderExtUriMid = "urn:ietf:params:rtp-hdrext:sdes:mid";
28+
const std::string rtpHeaderExtUriRid =
29+
"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id";
30+
31+
const std::string highRid = "h";
32+
const std::string medRid = "m";
33+
const std::string lowRid = "l";
34+
2735
WHIPOutput::WHIPOutput(obs_data_t *, obs_output_t *output)
2836
: output(output),
2937
endpoint_url(),
@@ -39,8 +47,7 @@ WHIPOutput::WHIPOutput(obs_data_t *, obs_output_t *output)
3947
total_bytes_sent(0),
4048
connect_time_ms(0),
4149
start_time_ns(0),
42-
last_audio_timestamp(0),
43-
last_video_timestamp(0)
50+
last_audio_timestamp(0)
4451
{
4552
}
4653

@@ -57,6 +64,27 @@ bool WHIPOutput::Start()
5764
{
5865
std::lock_guard<std::mutex> l(start_stop_mutex);
5966

67+
for (size_t idx = 0; idx < 5; idx++) {
68+
auto encoder = obs_output_get_video_encoder2(output, idx);
69+
if (encoder == nullptr) {
70+
break;
71+
}
72+
73+
auto v = std::make_shared<videoLayerState>();
74+
if (idx == 0) {
75+
v->ssrc = base_ssrc + 1;
76+
v->rid = highRid;
77+
} else if (idx == 1) {
78+
v->ssrc = base_ssrc + 2;
79+
v->rid = medRid;
80+
} else if (idx == 2) {
81+
v->ssrc = base_ssrc + 3;
82+
v->rid = lowRid;
83+
}
84+
85+
videoLayerStates[obs_encoder_get_width(encoder)] = v;
86+
}
87+
6088
if (!obs_output_can_begin_data_capture(output, 0))
6189
return false;
6290
if (!obs_output_initialize_encoders(output, 0))
@@ -92,10 +120,28 @@ void WHIPOutput::Data(struct encoder_packet *packet)
92120
audio_sr_reporter);
93121
last_audio_timestamp = packet->dts_usec;
94122
} else if (video_track && packet->type == OBS_ENCODER_VIDEO) {
95-
int64_t duration = packet->dts_usec - last_video_timestamp;
123+
auto rtp_config = video_sr_reporter->rtpConfig;
124+
auto videoLayerState =
125+
videoLayerStates[obs_encoder_get_width(packet->encoder)];
126+
if (videoLayerState == nullptr) {
127+
Stop(false);
128+
obs_output_signal_stop(output, OBS_OUTPUT_ENCODE_ERROR);
129+
return;
130+
}
131+
132+
rtp_config->sequenceNumber = videoLayerState->sequenceNumber;
133+
rtp_config->ssrc = videoLayerState->ssrc;
134+
rtp_config->rid = videoLayerState->rid;
135+
rtp_config->timestamp = videoLayerState->rtpTimestamp;
136+
int64_t duration =
137+
packet->dts_usec - videoLayerState->lastVideoTimestamp;
138+
96139
Send(packet->data, packet->size, duration, video_track,
97140
video_sr_reporter);
98-
last_video_timestamp = packet->dts_usec;
141+
142+
videoLayerState->sequenceNumber = rtp_config->sequenceNumber;
143+
videoLayerState->lastVideoTimestamp = packet->dts_usec;
144+
videoLayerState->rtpTimestamp = rtp_config->timestamp;
99145
}
100146
}
101147

@@ -151,9 +197,24 @@ void WHIPOutput::ConfigureVideoTrack(std::string media_stream_id,
151197
video_description.addSSRC(ssrc, cname, media_stream_id,
152198
media_stream_track_id);
153199

200+
video_description.addExtMap(
201+
rtc::Description::Entry::ExtMap(1, rtpHeaderExtUriMid));
202+
video_description.addExtMap(
203+
rtc::Description::Entry::ExtMap(2, rtpHeaderExtUriRid));
204+
205+
if (videoLayerStates.size() >= 2) {
206+
for (auto i = videoLayerStates.rbegin();
207+
i != videoLayerStates.rend(); i++) {
208+
video_description.addRid(i->second->rid);
209+
}
210+
}
211+
154212
auto rtp_config = std::make_shared<rtc::RtpPacketizationConfig>(
155213
ssrc, cname, video_payload_type,
156214
rtc::H264RtpPacketizer::defaultClockRate);
215+
rtp_config->midId = 1;
216+
rtp_config->ridId = 2;
217+
rtp_config->mid = video_mid;
157218

158219
const obs_encoder_t *encoder = obs_output_get_video_encoder2(output, 0);
159220
if (!encoder)
@@ -653,7 +714,7 @@ void WHIPOutput::StopThread(bool signal)
653714
connect_time_ms = 0;
654715
start_time_ns = 0;
655716
last_audio_timestamp = 0;
656-
last_video_timestamp = 0;
717+
videoLayerStates.clear();
657718
}
658719

659720
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,
697758

698759
void register_whip_output()
699760
{
700-
const uint32_t base_flags = OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE;
761+
const uint32_t base_flags = OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE |
762+
OBS_OUTPUT_MULTI_TRACK_AV;
701763

702764
const char *audio_codecs = "opus";
703765
#ifdef ENABLE_HEVC

plugins/obs-webrtc/whip-output.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313

1414
#include <rtc/rtc.hpp>
1515

16+
struct videoLayerState {
17+
uint16_t sequenceNumber;
18+
uint32_t rtpTimestamp;
19+
int64_t lastVideoTimestamp;
20+
uint32_t ssrc;
21+
std::string rid;
22+
};
23+
1624
class WHIPOutput {
1725
public:
1826
WHIPOutput(obs_data_t *settings, obs_output_t *output);
@@ -62,11 +70,12 @@ class WHIPOutput {
6270
std::shared_ptr<rtc::RtcpSrReporter> audio_sr_reporter;
6371
std::shared_ptr<rtc::RtcpSrReporter> video_sr_reporter;
6472

73+
std::map<uint32_t, std::shared_ptr<videoLayerState>> videoLayerStates;
74+
6575
std::atomic<size_t> total_bytes_sent;
6676
std::atomic<int> connect_time_ms;
6777
int64_t start_time_ns;
6878
int64_t last_audio_timestamp;
69-
int64_t last_video_timestamp;
7079
};
7180

7281
void register_whip_output();

0 commit comments

Comments
 (0)