From be191878fc00faff383f48e1fde48480fa7cc4cf Mon Sep 17 00:00:00 2001 From: Sean DuBois Date: Tue, 24 Dec 2024 00:10:02 -0500 Subject: [PATCH] obs-webrtc: Add Simulcast Support --- UI/data/locale/en-US.ini | 3 + UI/forms/OBSBasicSettings.ui | 85 ++++++++++++++++++ UI/simulcast-encoders.hpp | 69 +++++++++++++++ UI/window-basic-main-outputs.cpp | 27 ++++++ UI/window-basic-main-outputs.hpp | 5 ++ UI/window-basic-settings-stream.cpp | 21 ++++- UI/window-basic-settings.cpp | 1 + plugins/obs-webrtc/data/locale/en-US.ini | 1 + plugins/obs-webrtc/whip-output.cpp | 104 ++++++++++++++++++----- plugins/obs-webrtc/whip-output.h | 12 ++- plugins/obs-webrtc/whip-utils.h | 22 +++++ 11 files changed, 322 insertions(+), 28 deletions(-) create mode 100644 UI/simulcast-encoders.hpp diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 572b15f15117fb..6abe78ac0a7bba 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -985,6 +985,9 @@ Basic.Settings.Stream.MultitrackVideoStreamDumpEnable="Enable stream dump to FLV Basic.Settings.Stream.MultitrackVideoConfigOverride="Config Override (JSON)" Basic.Settings.Stream.MultitrackVideoConfigOverrideEnable="Enable Config Override" Basic.Settings.Stream.MultitrackVideoLabel="Multitrack Video" +Basic.Settings.Stream.SimulcastLabel="Simulcast" +Basic.Settings.Stream.SimulcastInfo="Simulcast allows you to encode and send multiple video qualities." +Basic.Settings.Stream.SimulcastTotalLayers="Total Layers" Basic.Settings.Stream.AdvancedOptions="Advanced Options" # basic mode 'output' settings diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index 267f4a3a4c5b79..644a06e3a70b87 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -1849,6 +1849,91 @@ + + + + Basic.Settings.Stream.SimulcastLabel + + + + 9 + + + 2 + + + 9 + + + 9 + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 170 + 10 + + + + + + + + Basic.Settings.Stream.SimulcastInfo + + + Qt::RichText + + + true + + + + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Basic.Settings.Stream.SimulcastTotalLayers + + + + + + + + + 1 + + + 4 + + + 1 + + + + + + + + + + diff --git a/UI/simulcast-encoders.hpp b/UI/simulcast-encoders.hpp new file mode 100644 index 00000000000000..448e78aa08ce49 --- /dev/null +++ b/UI/simulcast-encoders.hpp @@ -0,0 +1,69 @@ +#pragma once + +struct SimulcastEncoders { +public: + void Create(const char *encoderId, OBSBasic *main) + { + int rescaleFilter = config_get_int(main->Config(), "AdvOut", "RescaleFilter"); + if (rescaleFilter == OBS_SCALE_DISABLE) { + rescaleFilter = OBS_SCALE_BICUBIC; + } + + std::string encoder_name = "simulcast_0"; + auto simulcastTotalLayers = config_get_int(main->Config(), "Stream1", "SimulcastTotalLayers"); + if (simulcastTotalLayers <= 1) { + return; + } + + auto widthStep = video_output_get_width(obs_get_video()) / simulcastTotalLayers; + auto heightStep = video_output_get_height(obs_get_video()) / simulcastTotalLayers; + + for (auto i = simulcastTotalLayers - 1; i > 0; i--) { + uint32_t width = widthStep * i; + width -= width % 2; + + uint32_t height = heightStep * i; + height -= height % 2; + + encoder_name[encoder_name.size() - 1] = std::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 Update(obs_data_t *videoSettings, int videoBitrate) + { + auto bitrateStep = videoBitrate / static_cast(simulcastEncoders.size() + 1); + for (auto &simulcastEncoder : simulcastEncoders) { + videoBitrate -= bitrateStep; + obs_data_set_int(videoSettings, "bitrate", videoBitrate); + obs_encoder_update(simulcastEncoder, videoSettings); + } + } + + void SetVideoFormat(enum video_format format) + { + for (auto enc : simulcastEncoders) + obs_encoder_set_preferred_video_format(enc, format); + } + + void SetStreamOutput(obs_output_t *streamOutput) + { + for (size_t i = 0; i < simulcastEncoders.size(); i++) + obs_output_set_video_encoder2(streamOutput, simulcastEncoders[i], i + 1); + } + +private: + std::vector simulcastEncoders; +}; diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index 30d2031747dd5a..c530e85948e180 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -305,6 +305,10 @@ inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_) } if (multitrack_enabled) multitrackVideo = make_unique(); + + if (config_get_int(main->Config(), "Stream1", "SimulcastTotalLayers") > 1) { + simulcastEncoders = make_unique(); + }; } extern void log_vcam_changed(const VCamConfig &config, bool starting); @@ -541,6 +545,10 @@ void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId) if (!videoStreaming) throw "Failed to create video streaming encoder (simple output)"; obs_encoder_release(videoStreaming); + + if (simulcastEncoders != nullptr) { + simulcastEncoders->Create(encoderId, main); + } } /* mistakes have been made to lead us to this. */ @@ -800,11 +808,18 @@ void SimpleOutput::Update() break; default: obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12); + if (simulcastEncoders != nullptr) { + simulcastEncoders->SetVideoFormat(VIDEO_FORMAT_NV12); + } } obs_encoder_update(videoStreaming, videoSettings); obs_encoder_update(audioStreaming, audioSettings); obs_encoder_update(audioArchive, audioSettings); + + if (simulcastEncoders != nullptr) { + simulcastEncoders->Update(videoSettings, videoBitrate); + } } void SimpleOutput::UpdateRecordingAudioSettings() @@ -1094,6 +1109,9 @@ std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, Se } obs_output_set_video_encoder(streamOutput, videoStreaming); + if (simulcastEncoders != nullptr) { + simulcastEncoders->SetStreamOutput(streamOutput); + } obs_output_set_audio_encoder(streamOutput, audioStreaming, 0); obs_output_set_service(streamOutput, service); return true; @@ -1568,6 +1586,9 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_) throw "Failed to create streaming video encoder " "(advanced output)"; obs_encoder_release(videoStreaming); + if (simulcastEncoders != nullptr) { + simulcastEncoders->Create(streamEncoder, main); + } const char *rate_control = obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control"); @@ -1668,6 +1689,9 @@ void AdvancedOutput::UpdateStreamSettings() } obs_encoder_update(videoStreaming, settings); + if (simulcastEncoders != nullptr) { + simulcastEncoders->Update(settings, obs_data_get_int(settings, "bitrate")); + } } inline void AdvancedOutput::UpdateRecordingSettings() @@ -2082,6 +2106,9 @@ std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service, } obs_output_set_video_encoder(streamOutput, videoStreaming); + if (simulcastEncoders != nullptr) { + simulcastEncoders->SetStreamOutput(streamOutput); + } obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0); if (!is_multitrack_output) { diff --git a/UI/window-basic-main-outputs.hpp b/UI/window-basic-main-outputs.hpp index f7178f19de70aa..fc638203928695 100644 --- a/UI/window-basic-main-outputs.hpp +++ b/UI/window-basic-main-outputs.hpp @@ -5,6 +5,7 @@ #include #include "multitrack-video-output.hpp" +#include "simulcast-encoders.hpp" class OBSBasic; @@ -37,6 +38,8 @@ struct BasicOutputHandler { obs_scene_t *vCamSourceScene = nullptr; obs_sceneitem_t *vCamSourceSceneItem = nullptr; + std::unique_ptr simulcastEncoders; + std::string outputType; std::string lastError; @@ -99,6 +102,8 @@ 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 885c49f8a24ff2..36d24281b7d20c 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -102,6 +102,7 @@ void OBSBasicSettings::InitStreamPage() void OBSBasicSettings::LoadStream1Settings() { bool ignoreRecommended = config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + int simulcastTotalLayers = config_get_int(main->Config(), "Stream1", "SimulcastTotalLayers"); obs_service_t *service_obj = main->GetService(); const char *type = obs_service_get_type(service_obj); @@ -198,10 +199,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->simulcastGroupBox->show(); + } else { ui->key->setText(key); + ui->simulcastGroupBox->hide(); + } ServiceChanged(true); @@ -215,6 +219,7 @@ void OBSBasicSettings::LoadStream1Settings() ui->streamPage->setEnabled(!streamActive); ui->ignoreRecommended->setChecked(ignoreRecommended); + ui->simulcastTotalLayers->setValue(simulcastTotalLayers); loading = false; @@ -316,6 +321,9 @@ void OBSBasicSettings::SaveStream1Settings() SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended"); + auto oldSimulcastTotalLayers = config_get_int(main->Config(), "Stream1", "SimulcastTotalLayers"); + SaveSpinBox(ui->simulcastTotalLayers, "Stream1", "SimulcastTotalLayers"); + auto oldMultitrackVideoSetting = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo"); if (!IsCustomService()) { @@ -343,7 +351,8 @@ void OBSBasicSettings::SaveStream1Settings() SaveCheckBox(ui->multitrackVideoConfigOverrideEnable, "Stream1", "MultitrackVideoConfigOverrideEnabled"); SaveText(ui->multitrackVideoConfigOverride, "Stream1", "MultitrackVideoConfigOverride"); - if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked()) + if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked() || + oldSimulcastTotalLayers != ui->simulcastTotalLayers->value()) main->ResetOutputs(); SwapMultiTrack(QT_TO_UTF8(protocol)); @@ -576,6 +585,12 @@ void OBSBasicSettings::on_service_currentIndexChanged(int idx) } else { SwapMultiTrack(QT_TO_UTF8(protocol)); } + + if (IsWHIP()) { + ui->simulcastGroupBox->show(); + } else { + ui->simulcastGroupBox->hide(); + } } void OBSBasicSettings::on_customServer_textChanged(const QString &) diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index d8a35501b79ceb..72621e6b17b432 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -407,6 +407,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->simulcastTotalLayers, SCROLL_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/data/locale/en-US.ini b/plugins/obs-webrtc/data/locale/en-US.ini index c94717ec884366..246294691b4f92 100644 --- a/plugins/obs-webrtc/data/locale/en-US.ini +++ b/plugins/obs-webrtc/data/locale/en-US.ini @@ -4,3 +4,4 @@ Service.BearerToken="Bearer Token" Error.InvalidSDP="WHIP server responded with invalid SDP: %1" Error.NoRemoteDescription="Failed to set remote description: %1" +Error.SimulcastLayersRejected="WHIP server only accepted %1 simulcast layers" diff --git a/plugins/obs-webrtc/whip-output.cpp b/plugins/obs-webrtc/whip-output.cpp index 739161af4cf5be..07167a112fa983 100644 --- a/plugins/obs-webrtc/whip-output.cpp +++ b/plugins/obs-webrtc/whip-output.cpp @@ -24,6 +24,9 @@ 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"; + WHIPOutput::WHIPOutput(obs_data_t *, obs_output_t *output) : output(output), endpoint_url(), @@ -39,8 +42,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 +59,18 @@ bool WHIPOutput::Start() { std::lock_guard l(start_stop_mutex); + for (uint32_t idx = 0; idx < MAX_OUTPUT_VIDEO_ENCODERS; idx++) { + auto encoder = obs_output_get_video_encoder2(output, idx); + if (encoder == nullptr) { + break; + } + + auto v = std::make_shared(); + v->ssrc = base_ssrc + 1 + idx; + v->rid = std::to_string(idx); + videoLayerStates[encoder] = v; + } + if (!obs_output_can_begin_data_capture(output, 0)) return false; if (!obs_output_initialize_encoders(output, 0)) @@ -91,9 +105,25 @@ void WHIPOutput::Data(struct encoder_packet *packet) Send(packet->data, packet->size, duration, audio_track, 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[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; } } @@ -140,8 +170,20 @@ void WHIPOutput::ConfigureVideoTrack(std::string media_stream_id, std::string cn rtc::Description::Video video_description(video_mid, rtc::Description::Direction::SendOnly); 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) @@ -358,16 +400,26 @@ bool WHIPOutput::Connect() curl_easy_setopt(c, CURLOPT_UNRESTRICTED_AUTH, 1L); curl_easy_setopt(c, CURLOPT_ERRORBUFFER, error_buffer); - auto cleanup = [&]() { + auto cleanup = [&](bool connectFailed) { curl_easy_cleanup(c); curl_slist_free_all(headers); + if (connectFailed) { + obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + } + }; + + auto displayError = [&](const char *what, const char *errorMessage) { + struct dstr error_message; + dstr_init_copy(&error_message, obs_module_text(errorMessage)); + dstr_replace(&error_message, "%1", what); + obs_output_set_last_error(output, error_message.array); + dstr_free(&error_message); }; CURLcode res = curl_easy_perform(c); if (res != CURLE_OK) { do_log(LOG_ERROR, "Connect failed: %s", error_buffer[0] ? error_buffer : curl_easy_strerror(res)); - cleanup(); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + cleanup(true); return false; } @@ -375,15 +427,14 @@ bool WHIPOutput::Connect() curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &response_code); if (response_code != 201) { do_log(LOG_ERROR, "Connect failed: HTTP endpoint returned response code %ld", response_code); - cleanup(); + cleanup(false); obs_output_signal_stop(output, OBS_OUTPUT_INVALID_STREAM); return false; } if (read_buffer.empty()) { do_log(LOG_ERROR, "Connect failed: No data returned from HTTP endpoint request"); - cleanup(); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + cleanup(true); return false; } @@ -403,8 +454,7 @@ bool WHIPOutput::Connect() if (location_header_count < static_cast(redirect_count) + 1) { do_log(LOG_ERROR, "WHIP server did not provide a resource URL via the Location header"); - cleanup(); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + cleanup(true); return false; } @@ -432,8 +482,7 @@ bool WHIPOutput::Connect() curl_easy_getinfo(c, CURLINFO_EFFECTIVE_URL, &effective_url); if (effective_url == nullptr) { do_log(LOG_ERROR, "Failed to build Resource URL"); - cleanup(); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + cleanup(true); return false; } @@ -448,8 +497,7 @@ bool WHIPOutput::Connect() CURLUcode rc = curl_url_get(url_builder, CURLUPART_URL, &url, CURLU_NO_DEFAULT_PORT); if (rc) { do_log(LOG_ERROR, "WHIP server provided a invalid resource URL via the Location header"); - cleanup(); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + cleanup(true); return false; } @@ -465,31 +513,41 @@ bool WHIPOutput::Connect() auto response = std::string(read_buffer); response.erase(0, response.find("v=0")); + // If we are sending multiple layers assert + // that the remote accepted them all + if (videoLayerStates.size() != 1) { + auto layersAccepted = simulcast_layers_in_answer(response); + if (videoLayerStates.size() != layersAccepted) { + do_log(LOG_ERROR, "WHIP only accepted %lu layers", layersAccepted); + displayError(std::to_string(layersAccepted).c_str(), "Error.SimulcastLayersRejected"); + cleanup(true); + return false; + } + } + rtc::Description answer(response, "answer"); try { peer_connection->setRemoteDescription(answer); } catch (const std::invalid_argument &err) { do_log(LOG_ERROR, "WHIP server responded with invalid SDP: %s", err.what()); - cleanup(); + cleanup(true); struct dstr error_message; dstr_init_copy(&error_message, obs_module_text("Error.InvalidSDP")); dstr_replace(&error_message, "%1", err.what()); obs_output_set_last_error(output, error_message.array); dstr_free(&error_message); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); return false; } catch (const std::exception &err) { do_log(LOG_ERROR, "Failed to set remote description: %s", err.what()); - cleanup(); + cleanup(true); struct dstr error_message; dstr_init_copy(&error_message, obs_module_text("Error.NoRemoteDescription")); dstr_replace(&error_message, "%1", err.what()); obs_output_set_last_error(output, error_message.array); dstr_free(&error_message); - obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); return false; } - cleanup(); + cleanup(false); #if RTC_VERSION_MAJOR == 0 && RTC_VERSION_MINOR > 20 || RTC_VERSION_MAJOR > 1 peer_connection->gatherLocalCandidates(iceServers); @@ -597,7 +655,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, std::shared_ptr track, @@ -636,7 +694,7 @@ void WHIPOutput::Send(void *data, uintptr_t size, uint64_t duration, std::shared 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 2db60d65578fc3..d19a150931b807 100644 --- a/plugins/obs-webrtc/whip-output.h +++ b/plugins/obs-webrtc/whip-output.h @@ -13,6 +13,14 @@ #include +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); @@ -36,7 +44,6 @@ class WHIPOutput { void SendDelete(); void StopThread(bool signal); void ParseLinkHeader(std::string linkHeader, std::vector &iceServers); - void Send(void *data, uintptr_t size, uint64_t duration, std::shared_ptr track, std::shared_ptr rtcp_sr_reporter); @@ -58,11 +65,12 @@ class WHIPOutput { std::shared_ptr audio_sr_reporter; std::shared_ptr video_sr_reporter; + std::map> videoLayerStates; + std::atomic total_bytes_sent; std::atomic connect_time_ms; int64_t start_time_ns; int64_t last_audio_timestamp; - int64_t last_video_timestamp; }; void register_whip_output(); diff --git a/plugins/obs-webrtc/whip-utils.h b/plugins/obs-webrtc/whip-utils.h index 353cab65b5a525..c76f1f906ddfc4 100644 --- a/plugins/obs-webrtc/whip-utils.h +++ b/plugins/obs-webrtc/whip-utils.h @@ -83,3 +83,25 @@ static inline std::string generate_user_agent() return ua.str(); } + +static size_t simulcast_layers_in_answer(std::string answer) +{ + auto layersStart = answer.find("a=simulcast"); + if (layersStart == std::string::npos) { + return 0; + } + + auto layersEnd = answer.find("\r\n", layersStart); + if (layersEnd == std::string::npos) { + return 0; + } + + size_t layersAccepted = 1; + for (auto i = layersStart; i < layersEnd; i++) { + if (answer[i] == ';') { + layersAccepted++; + } + } + + return layersAccepted; +}