Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/data/locale/en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
94 changes: 94 additions & 0 deletions frontend/forms/OBSBasicSettings.ui
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
1 change: 1 addition & 0 deletions frontend/settings/OBSBasicSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
21 changes: 18 additions & 3 deletions frontend/settings/OBSBasicSettings_Stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);

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

ui->ignoreRecommended->setChecked(ignoreRecommended);
ui->whipSimulcastTotalLayers->setValue(whipSimulcastTotalLayers);

loading = false;

Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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 &)
Expand Down
12 changes: 12 additions & 0 deletions frontend/utility/AdvancedOutput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions frontend/utility/BasicOutputHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions frontend/utility/BasicOutputHandler.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <utility/MultitrackVideoOutput.hpp>
#include <utility/WHIPSimulcastEncoders.hpp>

#include <obs.hpp>
#include <util/dstr.hpp>
Expand Down Expand Up @@ -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;

Expand Down
17 changes: 17 additions & 0 deletions frontend/utility/SimpleOutput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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;
Expand Down
84 changes: 84 additions & 0 deletions frontend/utility/WHIPSimulcastEncoders.hpp
Original file line number Diff line number Diff line change
@@ -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;
};
1 change: 1 addition & 0 deletions plugins/obs-webrtc/data/locale/en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading
Loading