obs-webrtc: Add Simulcast Support

This commit is contained in:
Sean DuBois
2025-01-22 22:40:56 -05:00
committed by Ryan Foster
parent dcdbd2e904
commit cd4d624ec3
13 changed files with 364 additions and 32 deletions

View File

@@ -1014,6 +1014,9 @@ Basic.Settings.Stream.MultitrackVideoConfigOverride="Config Override (JSON)"
Basic.Settings.Stream.MultitrackVideoConfigOverrideEnable="Enable Config Override"
Basic.Settings.Stream.MultitrackVideoLabel="Multitrack Video"
Basic.Settings.Stream.MultitrackVideoExtraCanvas="Additional Canvas"
Basic.Settings.Stream.WHIPSimulcastLabel="Simulcast"
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>"
Basic.Settings.Stream.WHIPSimulcastTotalLayers="Total Layers"
Basic.Settings.Stream.AdvancedOptions="Advanced Options"
# basic mode 'output' settings

View File

@@ -2082,6 +2082,100 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="whipSimulcastGroupBox">
<property name="title">
<string>Basic.Settings.Stream.WHIPSimulcastLabel</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_35">
<property name="leftMargin">
<number>9</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_33">
<item>
<spacer name="horizontalSpacer_33">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>170</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="whipSimulcastInfo">
<property name="text">
<string>Basic.Settings.Stream.WHIPSimulcastInfo</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="formLayout_39">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="1" column="0">
<widget class="QLabel" name="whipSimulcastTotalLayersLabel">
<property name="text">
<string>Basic.Settings.Stream.WHIPSimulcastTotalLayers</string>
</property>
<property name="minimumSize">
<size>
<width>170</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_34" stretch="0,0">
<item>
<widget class="QSpinBox" name="whipSimulcastTotalLayers">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>4</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="serviceAdvancedOptionsGroupBox">
<property name="title">

View File

@@ -385,6 +385,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->whipSimulcastTotalLayers, 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);

View File

@@ -95,6 +95,7 @@ void OBSBasicSettings::InitStreamPage()
void OBSBasicSettings::LoadStream1Settings()
{
bool ignoreRecommended = config_get_bool(main->Config(), "Stream1", "IgnoreRecommended");
int whipSimulcastTotalLayers = config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers");
obs_service_t *service_obj = main->GetService();
const char *type = obs_service_get_type(service_obj);
@@ -209,10 +210,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->whipSimulcastGroupBox->show();
} else {
ui->key->setText(key);
ui->whipSimulcastGroupBox->hide();
}
ServiceChanged(true);
@@ -226,6 +230,7 @@ void OBSBasicSettings::LoadStream1Settings()
ui->streamPage->setEnabled(!streamActive);
ui->ignoreRecommended->setChecked(ignoreRecommended);
ui->whipSimulcastTotalLayers->setValue(whipSimulcastTotalLayers);
loading = false;
@@ -327,6 +332,9 @@ void OBSBasicSettings::SaveStream1Settings()
SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended");
auto oldWHIPSimulcastTotalLayers = config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers");
SaveSpinBox(ui->whipSimulcastTotalLayers, "Stream1", "WHIPSimulcastTotalLayers");
auto oldMultitrackVideoSetting = config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo");
if (!IsCustomService()) {
@@ -355,7 +363,8 @@ void OBSBasicSettings::SaveStream1Settings()
SaveText(ui->multitrackVideoConfigOverride, "Stream1", "MultitrackVideoConfigOverride");
SaveComboData(ui->multitrackVideoAdditionalCanvas, "Stream1", "MultitrackExtraCanvas");
if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked())
if (oldMultitrackVideoSetting != ui->enableMultitrackVideo->isChecked() ||
oldWHIPSimulcastTotalLayers != ui->whipSimulcastTotalLayers->value())
main->ResetOutputs();
SwapMultiTrack(QT_TO_UTF8(protocol));
@@ -588,6 +597,12 @@ void OBSBasicSettings::on_service_currentIndexChanged(int idx)
} else {
SwapMultiTrack(QT_TO_UTF8(protocol));
}
if (IsWHIP()) {
ui->whipSimulcastGroupBox->show();
} else {
ui->whipSimulcastGroupBox->hide();
}
}
void OBSBasicSettings::on_customServer_textChanged(const QString &)

View File

@@ -132,6 +132,12 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_)
throw "Failed to create streaming video encoder "
"(advanced output)";
obs_encoder_release(videoStreaming);
if (whipSimulcastEncoders != nullptr) {
whipSimulcastEncoders->Create(streamEncoder, config_get_int(main->Config(), "AdvOut", "RescaleFilter"),
config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers"),
video_output_get_width(obs_get_video()),
video_output_get_height(obs_get_video()));
}
const char *rate_control =
obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control");
@@ -247,6 +253,9 @@ void AdvancedOutput::UpdateStreamSettings()
}
obs_encoder_update(videoStreaming, settings);
if (whipSimulcastEncoders != nullptr) {
whipSimulcastEncoders->Update(settings, obs_data_get_int(settings, "bitrate"));
}
}
inline void AdvancedOutput::UpdateRecordingSettings()
@@ -649,6 +658,9 @@ std::shared_future<void> AdvancedOutput::SetupStreaming(obs_service_t *service,
}
obs_output_set_video_encoder(streamOutput, videoStreaming);
if (whipSimulcastEncoders != nullptr) {
whipSimulcastEncoders->SetStreamOutput(streamOutput);
}
obs_output_set_audio_encoder(streamOutput, streamAudioEnc, 0);
if (!is_multitrack_output) {

View File

@@ -236,6 +236,9 @@ BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_)
if (multitrack_enabled)
multitrackVideo = make_unique<MultitrackVideoOutput>();
if (config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers") > 1)
whipSimulcastEncoders = make_unique<WHIPSimulcastEncoders>();
}
extern void log_vcam_changed(const VCamConfig &config, bool starting);

View File

@@ -1,6 +1,7 @@
#pragma once
#include <utility/MultitrackVideoOutput.hpp>
#include <utility/WHIPSimulcastEncoders.hpp>
#include <obs.hpp>
#include <util/dstr.hpp>
@@ -42,6 +43,8 @@ struct BasicOutputHandler {
obs_scene_t *vCamSourceScene = nullptr;
obs_sceneitem_t *vCamSourceSceneItem = nullptr;
std::unique_ptr<WHIPSimulcastEncoders> whipSimulcastEncoders;
std::string outputType;
std::string lastError;

View File

@@ -75,6 +75,13 @@ void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId)
if (!videoStreaming)
throw "Failed to create video streaming encoder (simple output)";
obs_encoder_release(videoStreaming);
if (whipSimulcastEncoders != nullptr) {
whipSimulcastEncoders->Create(encoderId, config_get_int(main->Config(), "AdvOut", "RescaleFilter"),
config_get_int(main->Config(), "Stream1", "WHIPSimulcastTotalLayers"),
video_output_get_width(obs_get_video()),
video_output_get_height(obs_get_video()));
}
}
/* mistakes have been made to lead us to this. */
@@ -351,11 +358,18 @@ void SimpleOutput::Update()
break;
default:
obs_encoder_set_preferred_video_format(videoStreaming, VIDEO_FORMAT_NV12);
if (whipSimulcastEncoders != nullptr) {
whipSimulcastEncoders->SetVideoFormat(VIDEO_FORMAT_NV12);
}
}
obs_encoder_update(videoStreaming, videoSettings);
obs_encoder_update(audioStreaming, audioSettings);
obs_encoder_update(audioArchive, audioSettings);
if (whipSimulcastEncoders != nullptr) {
whipSimulcastEncoders->Update(videoSettings, videoBitrate);
}
}
void SimpleOutput::UpdateRecordingAudioSettings()
@@ -630,6 +644,9 @@ std::shared_future<void> SimpleOutput::SetupStreaming(obs_service_t *service, Se
}
obs_output_set_video_encoder(streamOutput, videoStreaming);
if (whipSimulcastEncoders != nullptr) {
whipSimulcastEncoders->SetStreamOutput(streamOutput);
}
obs_output_set_audio_encoder(streamOutput, audioStreaming, 0);
obs_output_set_service(streamOutput, service);
return true;

View File

@@ -0,0 +1,84 @@
/******************************************************************************
Copyright (C) 2025 by Sean DuBois <sean@pion.ly>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
struct WHIPSimulcastEncoders {
public:
void Create(const char *encoderId, int rescaleFilter, int whipSimulcastTotalLayers, uint32_t outputWidth,
uint32_t outputHeight)
{
if (rescaleFilter == OBS_SCALE_DISABLE) {
rescaleFilter = OBS_SCALE_BICUBIC;
}
if (whipSimulcastTotalLayers <= 1) {
return;
}
auto widthStep = outputWidth / whipSimulcastTotalLayers;
auto heightStep = outputHeight / whipSimulcastTotalLayers;
std::string encoder_name = "whip_simulcast_0";
for (auto i = whipSimulcastTotalLayers - 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 whip_simulcast_encoder =
obs_video_encoder_create(encoderId, encoder_name.c_str(), nullptr, nullptr);
if (whip_simulcast_encoder) {
obs_encoder_set_video(whip_simulcast_encoder, obs_get_video());
obs_encoder_set_scaled_size(whip_simulcast_encoder, width, height);
obs_encoder_set_gpu_scale_type(whip_simulcast_encoder, (obs_scale_type)rescaleFilter);
whipSimulcastEncoders.push_back(whip_simulcast_encoder);
obs_encoder_release(whip_simulcast_encoder);
} else {
blog(LOG_WARNING,
"Failed to create video streaming WHIP Simulcast encoders (BasicOutputHandler)");
}
}
}
void Update(obs_data_t *videoSettings, int videoBitrate)
{
auto bitrateStep = videoBitrate / static_cast<int>(whipSimulcastEncoders.size() + 1);
for (auto &whipSimulcastEncoder : whipSimulcastEncoders) {
videoBitrate -= bitrateStep;
obs_data_set_int(videoSettings, "bitrate", videoBitrate);
obs_encoder_update(whipSimulcastEncoder, videoSettings);
}
}
void SetVideoFormat(enum video_format format)
{
for (auto enc : whipSimulcastEncoders)
obs_encoder_set_preferred_video_format(enc, format);
}
void SetStreamOutput(obs_output_t *streamOutput)
{
for (size_t i = 0; i < whipSimulcastEncoders.size(); i++)
obs_output_set_video_encoder2(streamOutput, whipSimulcastEncoders[i], i + 1);
}
private:
std::vector<OBSEncoder> whipSimulcastEncoders;
};

View File

@@ -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"

View File

@@ -26,6 +26,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(),
@@ -41,8 +44,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)
{
}
@@ -59,6 +61,19 @@ bool WHIPOutput::Start()
{
std::lock_guard<std::mutex> 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<videoLayerState>();
// base_ssrc is ssrc for audio track. We do `+ 1` for the video, then idx for each Simulcast layer.
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))
@@ -93,9 +108,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;
}
}
@@ -142,6 +173,24 @@ 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) {
std::vector<std::pair<int, std::string>> sortedRids;
for (const auto &[encoder, state] : videoLayerStates) {
sortedRids.push_back({std::stoi(state->rid), state->rid});
}
std::sort(sortedRids.begin(), sortedRids.end(),
[](const auto &a, const auto &b) { return a.first < b.first; });
for (const auto &[_, rid] : sortedRids) {
video_description.addRid(rid);
}
}
auto rtp_config = std::make_shared<rtc::RtpPacketizationConfig>(ssrc, cname, video_payload_type,
#if RTC_VERSION_MAJOR == 0 && RTC_VERSION_MINOR > 22 || RTC_VERSION_MAJOR > 0
rtc::H264RtpPacketizer::ClockRate);
@@ -149,6 +198,10 @@ void WHIPOutput::ConfigureVideoTrack(std::string media_stream_id, std::string cn
rtc::H264RtpPacketizer::defaultClockRate);
#endif
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)
return;
@@ -372,16 +425,26 @@ bool WHIPOutput::Connect()
curl_easy_setopt(c, CURLOPT_UNRESTRICTED_AUTH, 1L);
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, error_buffer);
auto cleanup = [&]() {
auto doCleanup = [&](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);
doCleanup(true);
return false;
}
@@ -389,15 +452,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();
doCleanup(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);
doCleanup(true);
return false;
}
@@ -417,8 +479,7 @@ bool WHIPOutput::Connect()
if (location_header_count < static_cast<size_t>(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);
doCleanup(true);
return false;
}
@@ -446,8 +507,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);
doCleanup(true);
return false;
}
@@ -462,8 +522,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);
doCleanup(true);
return false;
}
@@ -479,31 +538,40 @@ 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");
doCleanup(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();
doCleanup(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();
doCleanup(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();
doCleanup(false);
#if RTC_VERSION_MAJOR == 0 && RTC_VERSION_MINOR > 20 || RTC_VERSION_MAJOR > 0
peer_connection->gatherLocalCandidates(iceServers);
@@ -557,7 +625,7 @@ void WHIPOutput::SendDelete()
curl_easy_setopt(c, CURLOPT_TIMEOUT, 8L);
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, error_buffer);
auto cleanup = [&]() {
auto doCleanup = [&]() {
curl_easy_cleanup(c);
curl_slist_free_all(headers);
};
@@ -566,7 +634,7 @@ void WHIPOutput::SendDelete()
if (res != CURLE_OK) {
do_log(LOG_WARNING, "DELETE request for resource URL failed: %s",
error_buffer[0] ? error_buffer : curl_easy_strerror(res));
cleanup();
doCleanup();
return;
}
@@ -574,13 +642,13 @@ void WHIPOutput::SendDelete()
curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &response_code);
if (response_code != 200) {
do_log(LOG_WARNING, "DELETE request for resource URL failed. HTTP Code: %ld", response_code);
cleanup();
doCleanup();
return;
}
do_log(LOG_DEBUG, "Successfully performed DELETE request for resource URL");
resource_url.clear();
cleanup();
doCleanup();
}
void WHIPOutput::StopThread(bool signal)
@@ -611,7 +679,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<rtc::Track> track,
@@ -652,7 +720,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

View File

@@ -10,9 +10,18 @@
#include <atomic>
#include <mutex>
#include <thread>
#include <algorithm>
#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);
@@ -36,7 +45,6 @@ private:
void SendDelete();
void StopThread(bool signal);
void ParseLinkHeader(std::string linkHeader, std::vector<rtc::IceServer> &iceServers);
void Send(void *data, uintptr_t size, uint64_t duration, std::shared_ptr<rtc::Track> track,
std::shared_ptr<rtc::RtcpSrReporter> rtcp_sr_reporter);
@@ -58,11 +66,12 @@ private:
std::shared_ptr<rtc::RtcpSrReporter> audio_sr_reporter;
std::shared_ptr<rtc::RtcpSrReporter> video_sr_reporter;
std::map<obs_encoder_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();

View File

@@ -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;
}