Skip to content

Commit

Permalink
obs-webrtc: Add Simulcast Support
Browse files Browse the repository at this point in the history
  • Loading branch information
Sean-Der committed Jun 18, 2024
1 parent 608d3bf commit eabfe74
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 13 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 @@ -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?"
Expand Down
7 changes: 7 additions & 0 deletions UI/forms/OBSBasicSettings.ui
Original file line number Diff line number Diff line change
Expand Up @@ -1884,6 +1884,13 @@
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="useSimulcast">
<property name="text">
<string>Basic.Settings.Stream.UseSimulcast</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
Expand Down
82 changes: 78 additions & 4 deletions UI/window-basic-main-outputs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,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 @@ -867,9 +871,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);
}
Expand Down Expand Up @@ -1189,8 +1198,13 @@ FutureHolder<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);
Expand Down Expand Up @@ -1729,6 +1743,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 @@ -1849,6 +1867,8 @@ void AdvancedOutput::UpdateStreamSettings()
}

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

inline void AdvancedOutput::UpdateRecordingSettings()
Expand Down Expand Up @@ -2338,8 +2358,13 @@ FutureHolder<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);
obs_output_set_audio_encoder(streamOutput,
streamAudioEnc, 0);

Expand Down Expand Up @@ -2886,3 +2911,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);
}
}
5 changes: 5 additions & 0 deletions UI/window-basic-main-outputs.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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 @@ -103,6 +105,9 @@ struct BasicOutputHandler {
std::string audio_encoder_id,
std::optional<size_t> vod_track_mixer);
OBSDataAutoRelease GenerateMultitrackVideoStreamDumpConfig();
void CreateSimulcastEncoders(const char *encoderId);
void SimulcastEncodersUpdate(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 @@ -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);
Expand Down Expand Up @@ -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);

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

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

loading = false;

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

SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended");
SaveCheckBox(ui->useSimulcast, "Stream1", "UseSimulcast");

auto oldMultitrackVideoSetting = config_get_bool(
main->Config(), "Stream1", "EnableMultitrackVideo");
Expand Down Expand Up @@ -661,6 +668,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 @@ -428,6 +428,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->useSimulcast, CHECK_CHANGED, OUTPUTS_CHANGED);
HookWidget(ui->outputMode, COMBO_CHANGED, OUTPUTS_CHANGED);
HookWidget(ui->simpleOutputPath, EDIT_CHANGED, OUTPUTS_CHANGED);
HookWidget(ui->simpleNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED);
Expand Down
74 changes: 68 additions & 6 deletions plugins/obs-webrtc/whip-output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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)
{
}

Expand All @@ -57,6 +64,27 @@ bool WHIPOutput::Start()
{
std::lock_guard<std::mutex> 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<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;
if (!obs_output_initialize_encoders(output, 0))
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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<rtc::RtpPacketizationConfig>(
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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion plugins/obs-webrtc/whip-output.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@

#include <rtc/rtc.hpp>

struct videoLayerState {
uint16_t sequenceNumber;
uint32_t rtpTimestamp;
int64_t lastVideoTimestamp;
uint32_t ssrc;
std::string rid;
};

class WHIPOutput {
public:
WHIPOutput(obs_data_t *settings, obs_output_t *output);
Expand Down Expand Up @@ -62,11 +70,12 @@ class WHIPOutput {
std::shared_ptr<rtc::RtcpSrReporter> audio_sr_reporter;
std::shared_ptr<rtc::RtcpSrReporter> video_sr_reporter;

std::map<uint32_t, std::shared_ptr<videoLayerState>> videoLayerStates;

std::atomic<size_t> total_bytes_sent;
std::atomic<int> connect_time_ms;
int64_t start_time_ns;
int64_t last_audio_timestamp;
int64_t last_video_timestamp;
};

void register_whip_output();

0 comments on commit eabfe74

Please sign in to comment.