mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-05-11 08:46:38 -04:00
obs-webrtc: Add Simulcast Support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 &)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
84
frontend/utility/WHIPSimulcastEncoders.hpp
Normal file
84
frontend/utility/WHIPSimulcastEncoders.hpp
Normal 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;
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user