diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini
index 572b15f15117fb..cb3dec5b12547f 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. No information about your computer and software setup will be sent."
+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/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp
index 30d2031747dd5a..b280b2ed0025ff 100644
--- a/UI/window-basic-main-outputs.cpp
+++ b/UI/window-basic-main-outputs.cpp
@@ -541,6 +541,8 @@ void SimpleOutput::LoadStreamingPreset_Lossy(const char *encoderId)
if (!videoStreaming)
throw "Failed to create video streaming encoder (simple output)";
obs_encoder_release(videoStreaming);
+
+ CreateSimulcastEncoders(encoderId);
}
/* mistakes have been made to lead us to this. */
@@ -800,11 +802,14 @@ void SimpleOutput::Update()
break;
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);
obs_encoder_update(audioStreaming, audioSettings);
obs_encoder_update(audioArchive, audioSettings);
+ SimulcastEncodersUpdate(videoSettings, videoBitrate);
}
void SimpleOutput::UpdateRecordingAudioSettings()
@@ -1094,6 +1099,8 @@ std::shared_future SimpleOutput::SetupStreaming(obs_service_t *service, Se
}
obs_output_set_video_encoder(streamOutput, videoStreaming);
+ 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);
return true;
@@ -1568,6 +1575,7 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_)
throw "Failed to create streaming video encoder "
"(advanced output)";
obs_encoder_release(videoStreaming);
+ CreateSimulcastEncoders(streamEncoder);
const char *rate_control =
obs_data_get_string(useStreamEncoder ? streamEncSettings : recordEncSettings, "rate_control");
@@ -1668,6 +1676,7 @@ void AdvancedOutput::UpdateStreamSettings()
}
obs_encoder_update(videoStreaming, settings);
+ SimulcastEncodersUpdate(settings, obs_data_get_int(settings, "bitrate"));
}
inline void AdvancedOutput::UpdateRecordingSettings()
@@ -2082,6 +2091,8 @@ std::shared_future AdvancedOutput::SetupStreaming(obs_service_t *service,
}
obs_output_set_video_encoder(streamOutput, videoStreaming);
+ 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);
if (!is_multitrack_output) {
@@ -2539,3 +2550,51 @@ 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";
+ 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] = 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)
+{
+ 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);
+ }
+}
diff --git a/UI/window-basic-main-outputs.hpp b/UI/window-basic-main-outputs.hpp
index f7178f19de70aa..232f39c05655d8 100644
--- a/UI/window-basic-main-outputs.hpp
+++ b/UI/window-basic-main-outputs.hpp
@@ -37,6 +37,8 @@ struct BasicOutputHandler {
obs_scene_t *vCamSourceScene = nullptr;
obs_sceneitem_t *vCamSourceSceneItem = nullptr;
+ std::vector simulcastEncoders;
+
std::string outputType;
std::string lastError;
@@ -60,7 +62,7 @@ struct BasicOutputHandler {
inline BasicOutputHandler(OBSBasic *main_);
- virtual ~BasicOutputHandler(){};
+ virtual ~BasicOutputHandler() {};
virtual std::shared_future SetupStreaming(obs_service_t *service,
SetupStreamingContinuation_t continuation) = 0;
@@ -99,6 +101,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..7cf9b0993b6540 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)
@@ -321,6 +363,28 @@ void WHIPOutput::ParseLinkHeader(std::string val, std::vector &i
}
}
+size_t WHIPOutput::CountSimulcastLayers(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;
+}
+
bool WHIPOutput::Connect()
{
struct curl_slist *headers = NULL;
@@ -358,16 +422,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 +449,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 +476,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 +504,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 +519,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 +535,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 = CountSimulcastLayers(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 +677,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 +716,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
@@ -672,7 +752,7 @@ void register_whip_output()
return obs_properties_create();
};
info.get_total_bytes = [](void *priv_data) -> uint64_t {
- return (uint64_t) static_cast(priv_data)->GetTotalBytes();
+ return (uint64_t)static_cast(priv_data)->GetTotalBytes();
};
info.get_connect_time_ms = [](void *priv_data) -> int {
return static_cast(priv_data)->GetConnectTime();
diff --git a/plugins/obs-webrtc/whip-output.h b/plugins/obs-webrtc/whip-output.h
index 2db60d65578fc3..1038893ccd3d39 100644
--- a/plugins/obs-webrtc/whip-output.h
+++ b/plugins/obs-webrtc/whip-output.h
@@ -13,6 +13,14 @@
#include