Skip to content

Commit

Permalink
obs-webrtc: Add WebRTC Simulcast Support
Browse files Browse the repository at this point in the history
  • Loading branch information
Sean-Der committed Apr 4, 2024
1 parent 80ad63a commit e65fbcf
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 12 deletions.
1 change: 1 addition & 0 deletions UI/data/locale/en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,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?"
Expand Down
10 changes: 10 additions & 0 deletions UI/forms/OBSBasicSettings.ui
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,16 @@
</property>
</widget>
</item>
<item row="13" column="1">
<widget class="QCheckBox" name="useSimulcast">
<property name="text">
<string>Basic.Settings.Stream.UseSimulcast</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_28">
<item>
Expand Down
83 changes: 81 additions & 2 deletions UI/window-basic-main-outputs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,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. */
Expand Down Expand Up @@ -844,9 +848,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);
ConfigureSimulcastEncoders(videoSettings, videoBitrate);

obs_encoder_update(audioStreaming, audioSettings);
obs_encoder_update(audioArchive, audioSettings);
}
Expand Down Expand Up @@ -1048,6 +1057,9 @@ inline void SimpleOutput::SetupOutputs()
{
SimpleOutput::Update();
obs_encoder_set_video(videoStreaming, obs_get_video());
for (auto enc : simulcastEncoders)
obs_encoder_set_video(enc, obs_get_video());

obs_encoder_set_audio(audioStreaming, obs_get_audio());
obs_encoder_set_audio(audioArchive, obs_get_audio());
int tracks =
Expand Down Expand Up @@ -1144,7 +1156,11 @@ bool SimpleOutput::SetupStreaming(obs_service_t *service)
outputType = type;
}

obs_output_set_video_encoder(streamOutput, videoStreaming);
obs_output_set_video_encoder2(streamOutput, videoStreaming, 0);
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;
Expand Down Expand Up @@ -1648,6 +1664,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");
Expand Down Expand Up @@ -1768,6 +1788,8 @@ void AdvancedOutput::UpdateStreamSettings()
}

obs_encoder_update(videoStreaming, settings);
ConfigureSimulcastEncoders(settings,
obs_data_get_int(settings, "bitrate"));
}

inline void AdvancedOutput::UpdateRecordingSettings()
Expand Down Expand Up @@ -2112,6 +2134,9 @@ inline void AdvancedOutput::UpdateAudioSettings()
void AdvancedOutput::SetupOutputs()
{
obs_encoder_set_video(videoStreaming, obs_get_video());
for (auto enc : simulcastEncoders)
obs_encoder_set_video(enc, obs_get_video());

if (videoRecording)
obs_encoder_set_video(videoRecording, obs_get_video());
for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) {
Expand Down Expand Up @@ -2228,7 +2253,11 @@ bool AdvancedOutput::SetupStreaming(obs_service_t *service)
outputType = type;
}

obs_output_set_video_encoder(streamOutput, videoStreaming);
obs_output_set_video_encoder2(streamOutput, videoStreaming, 0);
for (size_t i = 0; i < simulcastEncoders.size(); i++)
obs_output_set_video_encoder2(streamOutput,
simulcastEncoders[i], i + 1);

if (!is_multitrack_output) {
obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0);
} else {
Expand Down Expand Up @@ -2554,6 +2583,56 @@ std::string BasicOutputHandler::GetRecordingFilename(
return dst;
}

void BasicOutputHandler::CreateSimulcastEncoders(const char *encoderId)
{
std::string encoder_name = "simulcast_0";
for (auto i = 0; i < 2; i++) {
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) {
simulcastEncoders.push_back(simulcast_encoder);
obs_encoder_release(simulcast_encoder);
} else {
blog(LOG_WARNING,
"Failed to create video streaming simulcast encoders (simple output)");
}
}
}

void BasicOutputHandler::ConfigureSimulcastEncoders(obs_data_t *videoSettings,
int videoBitrate)
{
video_t *video = obs_get_video();

if (simulcastEncoders.size() > 0) {
uint32_t width = video_output_get_width(video) / 1.5;
width -= width % 2;

uint32_t height = video_output_get_height(video) / 1.5;
height -= height % 2;

obs_data_set_int(videoSettings, "bitrate", videoBitrate / 2);
obs_encoder_update(simulcastEncoders[0], videoSettings);
obs_encoder_set_scaled_size(simulcastEncoders[0], width,
height);
}

if (simulcastEncoders.size() > 1) {
uint32_t width = video_output_get_width(video) / 2;
width -= width % 2;

uint32_t height = video_output_get_height(video) / 2;
height -= height % 2;

obs_data_set_int(videoSettings, "bitrate", videoBitrate / 4);
obs_encoder_update(simulcastEncoders[1], videoSettings);
obs_encoder_set_scaled_size(simulcastEncoders[1], width,
height);
}
}

BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main)
{
return new SimpleOutput(main);
Expand Down
6 changes: 6 additions & 0 deletions UI/window-basic-main-outputs.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ struct BasicOutputHandler {
obs_scene_t *vCamSourceScene = nullptr;
obs_sceneitem_t *vCamSourceSceneItem = nullptr;

std::vector<OBSEncoder> simulcastEncoders;

std::string outputType;
std::string lastError;

Expand Down Expand Up @@ -79,6 +81,10 @@ struct BasicOutputHandler {
const char *container, bool noSpace,
bool overwrite, const char *format,
bool ffmpeg);

void CreateSimulcastEncoders(const char *encoderId);
void ConfigureSimulcastEncoders(obs_data_t *videoSettings,
int videoBitrate);
};

BasicOutputHandler *CreateSimpleOutputHandler(OBSBasic *main);
Expand Down
17 changes: 15 additions & 2 deletions UI/window-basic-settings-stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,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);
Expand Down Expand Up @@ -157,10 +159,13 @@ void OBSBasicSettings::LoadStream1Settings()
ui->server->setCurrentIndex(idx);
}

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);

Expand All @@ -173,6 +178,7 @@ void OBSBasicSettings::LoadStream1Settings()
ui->streamPage->setEnabled(!streamActive);

ui->ignoreRecommended->setChecked(ignoreRecommended);
ui->useSimulcast->setChecked(useSimulcast);

loading = false;

Expand Down Expand Up @@ -286,6 +292,7 @@ void OBSBasicSettings::SaveStream1Settings()
}

SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended");
SaveCheckBox(ui->useSimulcast, "Stream1", "UseSimulcast");
SwapMultiTrack(QT_TO_UTF8(protocol));
}

Expand Down Expand Up @@ -539,6 +546,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 &)
Expand Down
1 change: 1 addition & 0 deletions UI/window-basic-settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,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->outputMode, COMBO_CHANGED, OUTPUTS_CHANGED);
HookWidget(ui->simpleOutputPath, EDIT_CHANGED, OUTPUTS_CHANGED);
HookWidget(ui->simpleNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED);
Expand Down
78 changes: 71 additions & 7 deletions plugins/obs-webrtc/whip-output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const uint8_t audio_payload_type = 111;
const char *video_mid = "1";
const uint8_t video_payload_type = 96;

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),
is_av1(false),
Expand All @@ -37,8 +45,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)
{
}

Expand All @@ -60,7 +67,28 @@ bool WHIPOutput::Start()
return false;
}

is_av1 = (strcmp("av1", obs_encoder_get_codec(encoder)) == 0);
for (size_t idx = 0; idx < 5; idx++) {
auto encoder = obs_output_get_video_encoder2(output, idx);
if (encoder == nullptr) {
break;
}

is_av1 = (strcmp("av1", obs_encoder_get_codec(encoder)) == 0);

auto v = std::make_shared<videoLayerState>();
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;
Expand Down Expand Up @@ -97,10 +125,28 @@ void WHIPOutput::Data(struct encoder_packet *packet)
audio_sr_reporter);
last_audio_timestamp = packet->dts_usec;
} else if (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;
}
}

Expand Down Expand Up @@ -144,9 +190,26 @@ 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);
}
}

video_track = peer_connection->addTrack(video_description);

auto rtp_config = std::make_shared<rtc::RtpPacketizationConfig>(
ssrc, cname, video_payload_type,
rtc::H264RtpPacketizer::defaultClockRate);
rtp_config->midId = 1;
rtp_config->ridId = 2;
rtp_config->mid = video_mid;

if (is_av1) {
video_description.addAV1Codec(video_payload_type);
Expand Down Expand Up @@ -539,7 +602,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,
Expand Down Expand Up @@ -586,7 +649,8 @@ void register_whip_output()
struct obs_output_info info = {};

info.id = "whip_output";
info.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE;
info.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE |
OBS_OUTPUT_MULTI_TRACK_AV;
info.get_name = [](void *) -> const char * {
return obs_module_text("Output.Name");
};
Expand Down
Loading

0 comments on commit e65fbcf

Please sign in to comment.