diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 307d3666bd696b..cee510e9ac0dfa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ env: CEF_BUILD_VERSION_WIN: '5060' QT_VERSION_MAC: '6.4.3' QT_VERSION_WIN: '6.4.3' - DEPS_VERSION_WIN: '2023-04-12' + DEPS_VERSION_WIN: '2023-06-01' VLC_VERSION_WIN: '3.0.0-git' TWITCH_CLIENTID: ${{ secrets.TWITCH_CLIENT_ID }} TWITCH_HASH: ${{ secrets.TWITCH_HASH }} diff --git a/CI/linux/02_build_obs.sh b/CI/linux/02_build_obs.sh index b5cd618fbff064..9f81a651c5c631 100755 --- a/CI/linux/02_build_obs.sh +++ b/CI/linux/02_build_obs.sh @@ -59,6 +59,7 @@ _configure_obs() { -DLINUX_PORTABLE=${PORTABLE_BUILD:-OFF} \ -DENABLE_AJA=OFF \ -DENABLE_NEW_MPEGTS_OUTPUT=OFF \ + -DENABLE_WEBRTC=OFF \ ${PIPEWIRE_OPTION} \ ${YOUTUBE_OPTIONS} \ ${TWITCH_OPTIONS} \ diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index f06ea1314c448e..34e093deb9f952 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -208,6 +208,7 @@ Basic.AutoConfig.StreamPage.Server="Server" Basic.AutoConfig.StreamPage.StreamKey="Stream Key" Basic.AutoConfig.StreamPage.StreamKey.ToolTip="RIST: enter the encryption passphrase.\nRTMP: enter the key provided by the service.\nSRT: enter the streamid if the service uses one." Basic.AutoConfig.StreamPage.EncoderKey="Encoder Key" +Basic.AutoConfig.StreamPage.BearerToken="Bearer Token" Basic.AutoConfig.StreamPage.ConnectedAccount="Connected account" Basic.AutoConfig.StreamPage.PerformBandwidthTest="Estimate bitrate with bandwidth test (may take a few minutes)" Basic.AutoConfig.StreamPage.PreferHardwareEncoding="Prefer hardware encoding" diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 4b57ec2400c18c..b0ca8cac904842 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1369,7 +1369,8 @@ bool OBSBasic::LoadService() return false; /* Enforce Opus on FTL if needed */ - if (strcmp(obs_service_get_protocol(service), "FTL") == 0) { + if (strcmp(obs_service_get_protocol(service), "FTL") == 0 || + strcmp(obs_service_get_protocol(service), "WHIP") == 0) { const char *option = config_get_string( basicConfig, "SimpleOutput", "StreamAudioEncoder"); if (strcmp(option, "opus") != 0) diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index e167e31be270a0..ee035f6a93da17 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -29,6 +29,7 @@ extern QCefCookieManager *panel_cookies; enum class ListOpt : int { ShowAll = 1, Custom, + WHIP, }; enum class Section : int { @@ -41,6 +42,11 @@ inline bool OBSBasicSettings::IsCustomService() const return ui->service->currentData().toInt() == (int)ListOpt::Custom; } +inline bool OBSBasicSettings::IsWHIP() const +{ + return ui->service->currentData().toInt() == (int)ListOpt::WHIP; +} + void OBSBasicSettings::InitStreamPage() { ui->connectAccount2->setVisible(false); @@ -91,6 +97,9 @@ void OBSBasicSettings::LoadStream1Settings() obs_service_t *service_obj = main->GetService(); const char *type = obs_service_get_type(service_obj); + bool is_rtmp_custom = (strcmp(type, "rtmp_custom") == 0); + bool is_rtmp_common = (strcmp(type, "rtmp_common") == 0); + bool is_whip = (strcmp(type, "whip_custom") == 0); loading = true; @@ -100,10 +109,14 @@ void OBSBasicSettings::LoadStream1Settings() const char *server = obs_data_get_string(settings, "server"); const char *key = obs_data_get_string(settings, "key"); protocol = QT_UTF8(obs_service_get_protocol(service_obj)); + const char *bearer_token = + obs_data_get_string(settings, "bearer_token"); - if (strcmp(type, "rtmp_custom") == 0) { - ui->service->setCurrentIndex(0); + if (is_rtmp_custom || is_whip) ui->customServer->setText(server); + + if (is_rtmp_custom) { + ui->service->setCurrentIndex(0); lastServiceIdx = 0; lastCustomServer = ui->customServer->text(); @@ -157,7 +170,7 @@ void OBSBasicSettings::LoadStream1Settings() UpdateServerList(); - if (strcmp(type, "rtmp_common") == 0) { + if (is_rtmp_common) { int idx = ui->server->findData(server); if (idx == -1) { if (server && *server) @@ -167,7 +180,10 @@ void OBSBasicSettings::LoadStream1Settings() ui->server->setCurrentIndex(idx); } - ui->key->setText(key); + if (is_whip) + ui->key->setText(bearer_token); + else + ui->key->setText(key); lastService.clear(); ServiceChanged(); @@ -191,14 +207,21 @@ void OBSBasicSettings::LoadStream1Settings() void OBSBasicSettings::SaveStream1Settings() { bool customServer = IsCustomService(); - const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; + bool whip = IsWHIP(); + const char *service_id = "rtmp_common"; + + if (customServer) { + service_id = "rtmp_custom"; + } else if (whip) { + service_id = "whip_custom"; + } obs_service_t *oldService = main->GetService(); OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); OBSDataAutoRelease settings = obs_data_create(); - if (!customServer) { + if (!customServer && !whip) { obs_data_set_string(settings, "service", QT_TO_UTF8(ui->service->currentText())); obs_data_set_string(settings, "protocol", QT_TO_UTF8(protocol)); @@ -239,7 +262,12 @@ void OBSBasicSettings::SaveStream1Settings() obs_data_set_bool(settings, "bwtest", false); } - obs_data_set_string(settings, "key", QT_TO_UTF8(ui->key->text())); + if (whip) + obs_data_set_string(settings, "bearer_token", + QT_TO_UTF8(ui->key->text())); + else + obs_data_set_string(settings, "key", + QT_TO_UTF8(ui->key->text())); OBSServiceAutoRelease newService = obs_service_create( service_id, "default_service", settings, hotkeyData); @@ -262,7 +290,7 @@ void OBSBasicSettings::SaveStream1Settings() void OBSBasicSettings::UpdateMoreInfoLink() { - if (IsCustomService()) { + if (IsCustomService() || IsWHIP()) { ui->moreInfoButton->hide(); return; } @@ -312,6 +340,9 @@ void OBSBasicSettings::UpdateKeyLink() if (serviceName == "Dacast") { ui->streamKeyLabel->setText( QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); + } else if (IsWHIP()) { + ui->streamKeyLabel->setText( + QTStr("Basic.AutoConfig.StreamPage.BearerToken")); } else if (!IsCustomService()) { ui->streamKeyLabel->setText( QTStr("Basic.AutoConfig.StreamPage.StreamKey")); @@ -356,6 +387,8 @@ void OBSBasicSettings::LoadServices(bool showAll) for (QString &name : names) ui->service->addItem(name); + ui->service->insertItem(0, QTStr("WHIP"), QVariant((int)ListOpt::WHIP)); + if (!showAll) { ui->service->addItem( QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), @@ -484,6 +517,7 @@ void OBSBasicSettings::ServiceChanged() { std::string service = QT_TO_UTF8(ui->service->currentText()); bool custom = IsCustomService(); + bool whip = IsWHIP(); ui->disconnectAccount->setVisible(false); ui->bandwidthTestEnable->setVisible(false); @@ -500,7 +534,7 @@ void OBSBasicSettings::ServiceChanged() ui->authPwLabel->setVisible(custom); ui->authPwWidget->setVisible(custom); - if (custom) { + if (custom || whip) { ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget); @@ -625,11 +659,18 @@ void OBSBasicSettings::on_authPwShow_clicked() OBSService OBSBasicSettings::SpawnTempService() { bool custom = IsCustomService(); - const char *service_id = custom ? "rtmp_custom" : "rtmp_common"; + bool whip = IsWHIP(); + const char *service_id = "rtmp_common"; + + if (custom) { + service_id = "rtmp_custom"; + } else if (whip) { + service_id = "whip_custom"; + } OBSDataAutoRelease settings = obs_data_create(); - if (!custom) { + if (!custom && !whip) { obs_data_set_string(settings, "service", QT_TO_UTF8(ui->service->currentText())); obs_data_set_string( @@ -640,7 +681,13 @@ OBSService OBSBasicSettings::SpawnTempService() settings, "server", QT_TO_UTF8(ui->customServer->text().trimmed())); } - obs_data_set_string(settings, "key", QT_TO_UTF8(ui->key->text())); + + if (whip) + obs_data_set_string(settings, "bearer_token", + QT_TO_UTF8(ui->key->text())); + else + obs_data_set_string(settings, "key", + QT_TO_UTF8(ui->key->text())); OBSServiceAutoRelease newService = obs_service_create( service_id, "temp_service", settings, nullptr); diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index 33b5b107cc576a..ac99da9deaf7f1 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -258,6 +258,7 @@ class OBSBasicSettings : public QDialog { /* stream */ void InitStreamPage(); inline bool IsCustomService() const; + inline bool IsWHIP() const; void LoadServices(bool showAll); void OnOAuthStreamKeyConnected(); void OnAuthConnected(); diff --git a/build-aux/com.obsproject.Studio.json b/build-aux/com.obsproject.Studio.json index df8c209f27d9bb..56b0f5d239ec1b 100644 --- a/build-aux/com.obsproject.Studio.json +++ b/build-aux/com.obsproject.Studio.json @@ -53,7 +53,10 @@ "modules/20-x264.json", "modules/30-ffmpeg.json", "modules/40-luajit.json", + "modules/40-plog.json", + "modules/40-usrsctp.json", "modules/50-jansson.json", + "modules/50-libdatachannel.json", "modules/50-ntv2.json", "modules/50-pipewire.json", "modules/50-swig.json", diff --git a/build-aux/modules/40-plog.json b/build-aux/modules/40-plog.json new file mode 100644 index 00000000000000..1ca28e7b5b166b --- /dev/null +++ b/build-aux/modules/40-plog.json @@ -0,0 +1,18 @@ +{ + "name": "plog", + "buildsystem": "cmake-ninja", + "config-opts": [ + "-DPLOG_BUILD_SAMPLES=OFF" + ], + "cleanup": [ + "*" + ], + "sources": [ + { + "type": "git", + "url": "https://github.com/SergiusTheBest/plog.git", + "tag": "1.1.9", + "commit": "f47149410a4c927643148b96799f28b2d80d451b" + } + ] +} diff --git a/build-aux/modules/40-usrsctp.json b/build-aux/modules/40-usrsctp.json new file mode 100644 index 00000000000000..b9442f20978ed4 --- /dev/null +++ b/build-aux/modules/40-usrsctp.json @@ -0,0 +1,21 @@ +{ + "name": "usrsctp", + "buildsystem": "cmake-ninja", + "//": "Disable SCTP IP code. Packets are handle by WebRTC so we don't need it", + "config-opts": [ + "-Dsctp_build_shared_lib=ON", + "-Dsctp_build_programs=OFF", + "-Dsctp_inet=OFF", + "-Dsctp_inet6=OFF", + "-Dsctp_werror=OFF", + "-DCMAKE_POSITION_INDEPENDENT_CODE=ON" + ], + "sources": [ + { + "type": "git", + "url": "https://github.com/sctplab/usrsctp.git", + "tag": "0.9.5.0", + "commit": "07f871bda23943c43c9e74cc54f25130459de830" + } + ] +} diff --git a/build-aux/modules/50-libdatachannel.json b/build-aux/modules/50-libdatachannel.json new file mode 100644 index 00000000000000..a0ded6e0a65b83 --- /dev/null +++ b/build-aux/modules/50-libdatachannel.json @@ -0,0 +1,20 @@ +{ + "name": "libdatachannel", + "buildsystem": "cmake-ninja", + "config-opts": [ + "-DNO_EXAMPLES=ON", + "-DNO_TESTS=ON", + "-DNO_WEBSOCKET=ON", + "-DUSE_NICE=ON", + "-DPREFER_SYSTEM_LIB=ON" + ], + "sources": [ + { + "type": "git", + "url": "https://github.com/paullouisageneau/libdatachannel.git", + "disable-submodules": true, + "tag": "v0.19.0-alpha.1", + "commit": "f66f2813c11acaea3b20d9a5f115823777426e63" + } + ] +} diff --git a/buildspec.json b/buildspec.json index c764e153b6b386..d5858f4959a20b 100644 --- a/buildspec.json +++ b/buildspec.json @@ -1,29 +1,29 @@ { "dependencies": { "prebuilt": { - "version": "2023-04-12", + "version": "2023-06-01", "baseUrl": "https://github.com/obsproject/obs-deps/releases/download", "label": "Pre-Built obs-deps", "hashes": { - "macos-x86_64": "81120ffa33bb050c6c5fcd236e5cedfd7b80f7053fdba271fead5af20be0b5f5", - "macos-arm64": "b9bab79611774c4651d084e14259abe889d2b8d1653787a843d08cf3f0db881c", - "macos-universal": "9535c6e1ad96f7d49960251e85a245774088d48da1d602bb82f734b10219125a", - "windows-x64": "c13a14a1acc4224b21304d97b63da4121de1ed6981297e50496fbc474abc0503", - "linux-x86_64": "056425a8a7a4a0c242ed5ab9c1eba4dd6b004386877de4304524e7bea11c0ee2" + "macos-x86_64": "fdd3f85f597cb8237041673f4cb0b0908d548791126cfd0d12fa7886bea3745f", + "macos-arm64": "17a954636998b07355c66a3d242191bfde75467985cc64ab3c292859b1f54d28", + "macos-universal": "ecbfba9473abced9bd16ef2ac29ed61ce036c10a3135039d464a7611daf27fb8", + "windows-x64": "cacb858777edaa0251b90350192d175b9b977f186f872a16d1e6b67fc5b4f9f0", + "linux-x86_64": "b97ed74fde7a01ba8e90916318733e0d0bf89ed30e84a53d1ccd425f4afe5d7f" } }, "qt6": { - "version": "2023-04-12", + "version": "2023-06-01", "baseUrl": "https://github.com/obsproject/obs-deps/releases/download", "label": "Pre-Built Qt6", "hashes": { - "macos-x86_64": "2622d6ecd484a596da12b6a45b709fe563821eda558d0bbb27335c8c2f63945c", - "macos-arm64": "4f72aa1103d88a00d90c01bb3650276ffa1408e16ef40579177605483b986dc9", - "macos-universal": "eb7614544ab4f3d2c6052c797635602280ca5b028a6b987523d8484222ce45d1", - "windows-x64": "4d39364b8a8dee5aa24fcebd8440d5c22bb4551c6b440ffeacce7d61f2ed1add" + "macos-x86_64": "b874b9aefbb42e586661c8cc652c889413dd68ff683307de54af8074139e69ab", + "macos-arm64": "e95d6461b8cafea6125aa82e3f9888eea5c5101a3911e88543631fadeb5faa9f", + "macos-universal": "5c1880af8fe8e6a0e85924952e8c756410c37e6f13e36667be86cd0979a2ae8b", + "windows-x64": "aa77caa98d34263a97e0bd0435a18e5c981e915636af562557bb4bf09d49be04" }, "debugSymbols": { - "windows-x64": "f34ee5067be19ed370268b15c53684b7b8aaa867dc800b68931df905d679e31f" + "windows-x64": "7d955aca2b2976f9e085040795f5642b3cac1f76c5534a47fe8e6c6cf351a4fb" } }, "cef": { diff --git a/cmake/Modules/CopyMSVCBins.cmake b/cmake/Modules/CopyMSVCBins.cmake index 60e896386e99c3..295bd597528889 100644 --- a/cmake/Modules/CopyMSVCBins.cmake +++ b/cmake/Modules/CopyMSVCBins.cmake @@ -87,7 +87,9 @@ file( "${FFMPEG_avcodec_INCLUDE_DIR}/../bin${_bin_suffix}/libbz2*.dll" "${FFMPEG_avcodec_INCLUDE_DIR}/../bin${_bin_suffix}/zlib*.dll" "${FFMPEG_avcodec_INCLUDE_DIR}/bin${_bin_suffix}/libbz2*.dll" - "${FFMPEG_avcodec_INCLUDE_DIR}/bin${_bin_suffix}/zlib*.dll") + "${FFMPEG_avcodec_INCLUDE_DIR}/bin${_bin_suffix}/zlib*.dll" + "${FFMPEG_avcodec_INCLUDE_DIR}/../bin/libdatachannel*.dll" + "${FFMPEG_avcodec_INCLUDE_DIR}/bin/libdatachannel*.dll") file(GLOB X264_BIN_FILES "${X264_INCLUDE_DIR}/../bin${_bin_suffix}/libx264-*.dll" "${X264_INCLUDE_DIR}/../bin/libx264-*.dll" "${X264_INCLUDE_DIR}/bin/libx264-*.dll" diff --git a/libobs/obs-service.h b/libobs/obs-service.h index 709f1c2c5242af..698b1c43162a8e 100644 --- a/libobs/obs-service.h +++ b/libobs/obs-service.h @@ -42,6 +42,7 @@ enum obs_service_connect_info { OBS_SERVICE_CONNECT_INFO_USERNAME = 4, OBS_SERVICE_CONNECT_INFO_PASSWORD = 6, OBS_SERVICE_CONNECT_INFO_ENCRYPT_PASSPHRASE = 8, + OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN = 10, }; struct obs_service_info { diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f928f772c564dc..6013cd5a7f9190 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -76,6 +76,7 @@ if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) OR OS_LINUX) add_subdirectory(obs-vst) endif() + add_subdirectory(obs-webrtc) check_obs_websocket() add_subdirectory(obs-x264) add_subdirectory(rtmp-services) @@ -191,3 +192,4 @@ add_subdirectory(obs-transitions) add_subdirectory(rtmp-services) add_subdirectory(text-freetype2) add_subdirectory(aja) +add_subdirectory(obs-webrtc) diff --git a/plugins/obs-webrtc/CMakeLists.txt b/plugins/obs-webrtc/CMakeLists.txt new file mode 100644 index 00000000000000..8ddd0598e4b972 --- /dev/null +++ b/plugins/obs-webrtc/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +legacy_check() + +option(ENABLE_WEBRTC "Enable WebRTC Output support" ON) +if(NOT ENABLE_WEBRTC) + message(STATUS "OBS: DISABLED obs-webrtc") + return() +endif() + +find_package(LibDataChannel REQUIRED) +find_package(CURL REQUIRED) + +add_library(obs-webrtc MODULE) +add_library(OBS::webrtc ALIAS obs-webrtc) + +target_sources(obs-webrtc PRIVATE obs-webrtc.cpp whip-output.cpp whip-output.h whip-service.cpp whip-service.h) + +target_link_libraries(obs-webrtc PRIVATE OBS::libobs LibDataChannel::LibDataChannel CURL::libcurl) + +set_target_properties_obs(obs-webrtc PROPERTIES FOLDER plugins PREFIX "") diff --git a/plugins/obs-webrtc/cmake/legacy.cmake b/plugins/obs-webrtc/cmake/legacy.cmake new file mode 100644 index 00000000000000..934b9c9eb69c79 --- /dev/null +++ b/plugins/obs-webrtc/cmake/legacy.cmake @@ -0,0 +1,21 @@ +project(obs-webrtc) + +option(ENABLE_WEBRTC "Enable WebRTC Output support" ON) +if(NOT ENABLE_WEBRTC) + obs_status(DISABLED, "obs-webrtc") + return() +endif() + +find_package(LibDataChannel REQUIRED) +find_package(CURL REQUIRED) + +add_library(obs-webrtc MODULE) +add_library(OBS::webrtc ALIAS obs-webrtc) + +target_sources(obs-webrtc PRIVATE obs-webrtc.cpp whip-output.cpp whip-output.h whip-service.cpp whip-service.h) + +target_link_libraries(obs-webrtc PRIVATE OBS::libobs LibDataChannel::LibDataChannel CURL::libcurl) + +set_target_properties(obs-webrtc PROPERTIES FOLDER "plugins") + +setup_plugin_target(obs-webrtc) diff --git a/plugins/obs-webrtc/cmake/macos/Info.plist.in b/plugins/obs-webrtc/cmake/macos/Info.plist.in new file mode 100644 index 00000000000000..bc130c522ade4c --- /dev/null +++ b/plugins/obs-webrtc/cmake/macos/Info.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleName + obs-webrtc + CFBundleIdentifier + com.obsproject.obs-webrtc + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + obs-webrtc + CFBundlePackageType + BNDL + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + (c) 2012-${CURRENT_YEAR} Hugh Bailey + + diff --git a/plugins/obs-webrtc/cmake/windows/obs-module.rc.in b/plugins/obs-webrtc/cmake/windows/obs-module.rc.in new file mode 100644 index 00000000000000..ab2a464310e0b3 --- /dev/null +++ b/plugins/obs-webrtc/cmake/windows/obs-module.rc.in @@ -0,0 +1,24 @@ +1 VERSIONINFO +FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "${OBS_COMPANY_NAME}" + VALUE "FileDescription", "OBS output module" + VALUE "FileVersion", "${OBS_VERSION_CANONICAL}" + VALUE "ProductName", "${OBS_PRODUCT_NAME}" + VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}" + VALUE "Comments", "${OBS_COMMENTS}" + VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}" + VALUE "InternalName", "obs-webrtc" + VALUE "OriginalFilename", "obs-webrtc" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/plugins/obs-webrtc/data/locale/en-US.ini b/plugins/obs-webrtc/data/locale/en-US.ini new file mode 100644 index 00000000000000..617c20e2ee5d42 --- /dev/null +++ b/plugins/obs-webrtc/data/locale/en-US.ini @@ -0,0 +1,3 @@ +Output.Name="WHIP Output" +Service.Name="WHIP Service" +Service.BearerToken="Bearer Token" diff --git a/plugins/obs-webrtc/obs-webrtc.cpp b/plugins/obs-webrtc/obs-webrtc.cpp new file mode 100644 index 00000000000000..ebb2eb4fdc751e --- /dev/null +++ b/plugins/obs-webrtc/obs-webrtc.cpp @@ -0,0 +1,19 @@ +#include + +#include "whip-output.h" +#include "whip-service.h" + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("obs-webrtc", "en-US") +MODULE_EXPORT const char *obs_module_description(void) +{ + return "OBS WebRTC module"; +} + +bool obs_module_load() +{ + register_whip_output(); + register_whip_service(); + + return true; +} diff --git a/plugins/obs-webrtc/whip-output.cpp b/plugins/obs-webrtc/whip-output.cpp new file mode 100644 index 00000000000000..8d45b7257111c2 --- /dev/null +++ b/plugins/obs-webrtc/whip-output.cpp @@ -0,0 +1,487 @@ +#include "whip-output.h" + +const int signaling_media_id_length = 16; +const char signaling_media_id_valid_char[] = "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + +const uint32_t audio_ssrc = 5002; +const char *audio_mid = "0"; +const uint32_t audio_clockrate = 48000; +const uint8_t audio_payload_type = 111; + +const uint32_t video_ssrc = 5000; +const char *video_mid = "1"; +const uint32_t video_clockrate = 90000; +const uint8_t video_payload_type = 96; + +WHIPOutput::WHIPOutput(obs_data_t *, obs_output_t *output) + : output(output), + endpoint_url(), + bearer_token(), + resource_url(), + running(false), + start_stop_mutex(), + start_stop_thread(), + peer_connection(-1), + audio_track(-1), + video_track(-1), + total_bytes_sent(0), + connect_time_ms(0), + start_time_ns(0), + last_audio_timestamp(0), + last_video_timestamp(0) +{ +} + +WHIPOutput::~WHIPOutput() +{ + Stop(); + + std::lock_guard l(start_stop_mutex); + if (start_stop_thread.joinable()) + start_stop_thread.join(); +} + +bool WHIPOutput::Start() +{ + std::lock_guard l(start_stop_mutex); + + if (!obs_output_can_begin_data_capture2(output)) + return false; + if (!obs_output_initialize_encoders2(output)) + return false; + + if (start_stop_thread.joinable()) + start_stop_thread.join(); + start_stop_thread = std::thread(&WHIPOutput::StartThread, this); + + return true; +} + +void WHIPOutput::Stop(bool signal) +{ + std::lock_guard l(start_stop_mutex); + if (start_stop_thread.joinable()) + start_stop_thread.join(); + + start_stop_thread = std::thread(&WHIPOutput::StopThread, this, signal); +} + +void WHIPOutput::Data(struct encoder_packet *packet) +{ + if (!packet) { + Stop(false); + obs_output_signal_stop(output, OBS_OUTPUT_ENCODE_ERROR); + return; + } + + if (packet->type == OBS_ENCODER_AUDIO) { + int64_t duration = packet->dts_usec - last_audio_timestamp; + Send(packet->data, packet->size, duration, audio_track); + last_audio_timestamp = packet->dts_usec; + } else if (packet->type == OBS_ENCODER_VIDEO) { + int64_t duration = packet->dts_usec - last_video_timestamp; + Send(packet->data, packet->size, duration, video_track); + last_video_timestamp = packet->dts_usec; + } +} + +void WHIPOutput::ConfigureAudioTrack(std::string media_stream_id, + std::string cname) +{ + auto media_stream_track_id = std::string(media_stream_id + "-audio"); + + rtcTrackInit track_init = { + RTC_DIRECTION_SENDONLY, + RTC_CODEC_OPUS, + audio_payload_type, + audio_ssrc, + audio_mid, + cname.c_str(), + media_stream_id.c_str(), + media_stream_track_id.c_str(), + }; + + rtcPacketizationHandlerInit packetizer_init = {audio_ssrc, + cname.c_str(), + audio_payload_type, + audio_clockrate, + 0, + 0, + RTC_NAL_SEPARATOR_LENGTH, + 0}; + + audio_track = rtcAddTrackEx(peer_connection, &track_init); + rtcSetOpusPacketizationHandler(audio_track, &packetizer_init); + rtcChainRtcpSrReporter(audio_track); + rtcChainRtcpNackResponder(audio_track, 1000); +} + +void WHIPOutput::ConfigureVideoTrack(std::string media_stream_id, + std::string cname) +{ + auto media_stream_track_id = std::string(media_stream_id + "-video"); + + rtcTrackInit track_init = { + RTC_DIRECTION_SENDONLY, + RTC_CODEC_H264, + video_payload_type, + video_ssrc, + video_mid, + cname.c_str(), + media_stream_id.c_str(), + media_stream_track_id.c_str(), + }; + + rtcPacketizationHandlerInit packetizer_init = { + video_ssrc, + cname.c_str(), + video_payload_type, + video_clockrate, + 0, + 0, + RTC_NAL_SEPARATOR_START_SEQUENCE, + 0}; + + video_track = rtcAddTrackEx(peer_connection, &track_init); + rtcSetH264PacketizationHandler(video_track, &packetizer_init); + rtcChainRtcpSrReporter(video_track); + rtcChainRtcpNackResponder(video_track, 1000); +} + +bool WHIPOutput::Setup() +{ + obs_service_t *service = obs_output_get_service(output); + if (!service) { + obs_output_signal_stop(output, OBS_OUTPUT_ERROR); + return false; + } + + endpoint_url = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_SERVER_URL); + if (endpoint_url.empty()) { + obs_output_signal_stop(output, OBS_OUTPUT_BAD_PATH); + return false; + } + bearer_token = obs_service_get_connect_info( + service, OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN); + + rtcConfiguration config; + memset(&config, 0, sizeof(config)); + + peer_connection = rtcCreatePeerConnection(&config); + rtcSetUserPointer(peer_connection, this); + + rtcSetStateChangeCallback(peer_connection, [](int, rtcState state, + void *ptr) { + auto whipOutput = static_cast(ptr); + switch (state) { + case RTC_NEW: + do_log_s(LOG_INFO, "PeerConnection state is now: New"); + break; + case RTC_CONNECTING: + do_log_s(LOG_INFO, + "PeerConnection state is now: Connecting"); + whipOutput->start_time_ns = os_gettime_ns(); + break; + case RTC_CONNECTED: + do_log_s(LOG_INFO, + "PeerConnection state is now: Connected"); + whipOutput->connect_time_ms = + (int)((os_gettime_ns() - + whipOutput->start_time_ns) / + 1000000.0); + do_log_s(LOG_INFO, "Connect time: %dms", + whipOutput->connect_time_ms.load()); + break; + case RTC_DISCONNECTED: + do_log_s(LOG_INFO, + "PeerConnection state is now: Disconnected"); + whipOutput->Stop(false); + obs_output_signal_stop(whipOutput->output, + OBS_OUTPUT_DISCONNECTED); + break; + case RTC_FAILED: + do_log_s(LOG_INFO, + "PeerConnection state is now: Failed"); + whipOutput->Stop(false); + obs_output_signal_stop(whipOutput->output, + OBS_OUTPUT_ERROR); + break; + case RTC_CLOSED: + do_log_s(LOG_INFO, + "PeerConnection state is now: Closed"); + break; + } + }); + + std::string media_stream_id, cname; + media_stream_id.reserve(signaling_media_id_length); + cname.reserve(signaling_media_id_length); + + for (int i = 0; i < signaling_media_id_length; ++i) { + media_stream_id += signaling_media_id_valid_char + [rand() % (sizeof(signaling_media_id_valid_char) - 1)]; + + cname += signaling_media_id_valid_char + [rand() % (sizeof(signaling_media_id_valid_char) - 1)]; + } + + ConfigureAudioTrack(media_stream_id, cname); + ConfigureVideoTrack(media_stream_id, cname); + + rtcSetLocalDescription(peer_connection, "offer"); + + return true; +} + +bool WHIPOutput::Connect() +{ + struct curl_slist *headers = NULL; + headers = curl_slist_append(headers, "Content-Type: application/sdp"); + if (!bearer_token.empty()) { + auto bearer_token_header = + std::string("Authorization: Bearer ") + bearer_token; + headers = + curl_slist_append(headers, bearer_token_header.c_str()); + } + + std::string read_buffer; + std::string location_header; + char offer_sdp[4096] = {0}; + rtcGetLocalDescription(peer_connection, offer_sdp, sizeof(offer_sdp)); + + CURL *c = curl_easy_init(); + curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, curl_writefunction); + curl_easy_setopt(c, CURLOPT_WRITEDATA, (void *)&read_buffer); + curl_easy_setopt(c, CURLOPT_HEADERFUNCTION, curl_headerfunction); + curl_easy_setopt(c, CURLOPT_HEADERDATA, (void *)&location_header); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(c, CURLOPT_URL, endpoint_url.c_str()); + curl_easy_setopt(c, CURLOPT_POST, 1L); + curl_easy_setopt(c, CURLOPT_COPYPOSTFIELDS, offer_sdp); + curl_easy_setopt(c, CURLOPT_TIMEOUT, 8L); + + auto cleanup = [&]() { + curl_easy_cleanup(c); + curl_slist_free_all(headers); + }; + + CURLcode res = curl_easy_perform(c); + if (res != CURLE_OK) { + do_log(LOG_WARNING, + "Connect failed: CURL returned result not CURLE_OK"); + cleanup(); + obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + return false; + } + + long response_code; + curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &response_code); + if (response_code != 201) { + do_log(LOG_WARNING, + "Connect failed: HTTP endpoint returned response code %ld", + response_code); + cleanup(); + obs_output_signal_stop(output, OBS_OUTPUT_INVALID_STREAM); + return false; + } + + if (read_buffer.empty()) { + do_log(LOG_WARNING, + "Connect failed: No data returned from HTTP endpoint request"); + cleanup(); + obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED); + return false; + } + + if (location_header.empty()) { + do_log(LOG_WARNING, + "WHIP server did not provide a resource URL via the Location header"); + } else { + CURLU *h = curl_url(); + curl_url_set(h, CURLUPART_URL, endpoint_url.c_str(), 0); + curl_url_set(h, CURLUPART_URL, location_header.c_str(), 0); + char *url = nullptr; + CURLUcode rc = curl_url_get(h, CURLUPART_URL, &url, + CURLU_NO_DEFAULT_PORT); + if (!rc) { + resource_url = url; + curl_free(url); + do_log(LOG_DEBUG, "WHIP Resource URL is: %s", + resource_url.c_str()); + } else { + do_log(LOG_WARNING, + "Unable to process resource URL response"); + } + curl_url_cleanup(h); + } + + rtcSetRemoteDescription(peer_connection, read_buffer.c_str(), "answer"); + cleanup(); + return true; +} + +void WHIPOutput::StartThread() +{ + if (!Setup()) + return; + + if (!Connect()) { + rtcDeletePeerConnection(peer_connection); + peer_connection = -1; + audio_track = -1; + video_track = -1; + return; + } + + obs_output_begin_data_capture2(output); + running = true; +} + +void WHIPOutput::SendDelete() +{ + if (resource_url.empty()) { + do_log(LOG_DEBUG, + "No resource URL available, not sending DELETE"); + return; + } + + struct curl_slist *headers = NULL; + if (!bearer_token.empty()) { + auto bearer_token_header = + std::string("Authorization: Bearer ") + bearer_token; + headers = + curl_slist_append(headers, bearer_token_header.c_str()); + } + + CURL *c = curl_easy_init(); + curl_easy_setopt(c, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(c, CURLOPT_URL, resource_url.c_str()); + curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); + curl_easy_setopt(c, CURLOPT_TIMEOUT, 8L); + + auto cleanup = [&]() { + curl_easy_cleanup(c); + curl_slist_free_all(headers); + }; + + CURLcode res = curl_easy_perform(c); + if (res != CURLE_OK) { + do_log(LOG_WARNING, + "DELETE request for resource URL failed. Reason: %s", + curl_easy_strerror(res)); + cleanup(); + return; + } + + long response_code; + 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(); + return; + } + + do_log(LOG_DEBUG, + "Successfully performed DELETE request for resource URL"); + resource_url.clear(); + cleanup(); +} + +void WHIPOutput::StopThread(bool signal) +{ + if (peer_connection != -1) { + rtcDeletePeerConnection(peer_connection); + peer_connection = -1; + audio_track = -1; + video_track = -1; + } + + SendDelete(); + + // "signal" exists because we have to preserve the "running" state + // across reconnect attempts. If we don't emit a signal if + // something calls obs_output_stop() and it's reconnecting, you'll + // desync the UI, as the output will be "stopped" and not + // "reconnecting", but the "stop" signal will have never been + // emitted. + if (running && signal) { + obs_output_signal_stop(output, OBS_OUTPUT_SUCCESS); + running = false; + } + + total_bytes_sent = 0; + connect_time_ms = 0; + start_time_ns = 0; + last_audio_timestamp = 0; + last_video_timestamp = 0; +} + +void WHIPOutput::Send(void *data, uintptr_t size, uint64_t duration, int track) +{ + if (!running) + return; + + // sample time is in us, we need to convert it to seconds + auto elapsed_seconds = double(duration) / (1000.0 * 1000.0); + + // get elapsed time in clock rate + uint32_t elapsed_timestamp = 0; + rtcTransformSecondsToTimestamp(track, elapsed_seconds, + &elapsed_timestamp); + + // set new timestamp + uint32_t current_timestamp = 0; + rtcGetCurrentTrackTimestamp(track, ¤t_timestamp); + rtcSetTrackRtpTimestamp(track, current_timestamp + elapsed_timestamp); + + total_bytes_sent += size; + rtcSendMessage(track, reinterpret_cast(data), (int)size); +} + +void register_whip_output() +{ + struct obs_output_info info = {}; + + info.id = "whip_output"; + info.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE; + info.get_name = [](void *) -> const char * { + return obs_module_text("Output.Name"); + }; + info.create = [](obs_data_t *settings, obs_output_t *output) -> void * { + return new WHIPOutput(settings, output); + }; + info.destroy = [](void *priv_data) { + delete static_cast(priv_data); + }; + info.start = [](void *priv_data) -> bool { + return static_cast(priv_data)->Start(); + }; + info.stop = [](void *priv_data, uint64_t) { + static_cast(priv_data)->Stop(); + }; + info.encoded_packet = [](void *priv_data, + struct encoder_packet *packet) { + static_cast(priv_data)->Data(packet); + }; + info.get_defaults = [](obs_data_t *) {}; + info.get_properties = [](void *) -> obs_properties_t * { + return obs_properties_create(); + }; + info.get_total_bytes = [](void *priv_data) -> uint64_t { + return (uint64_t) static_cast(priv_data) + ->GetTotalBytes(); + }; + info.get_connect_time_ms = [](void *priv_data) -> int { + return static_cast(priv_data)->GetConnectTime(); + }; + info.encoded_video_codecs = "h264"; + info.encoded_audio_codecs = "opus"; + info.protocols = "WHIP"; + + obs_register_output(&info); +} diff --git a/plugins/obs-webrtc/whip-output.h b/plugins/obs-webrtc/whip-output.h new file mode 100644 index 00000000000000..b8b10b5323e1ab --- /dev/null +++ b/plugins/obs-webrtc/whip-output.h @@ -0,0 +1,111 @@ +#pragma once +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define do_log(level, format, ...) \ + blog(level, "[obs-webrtc] [whip_output: '%s'] " format, \ + obs_output_get_name(output), ##__VA_ARGS__) +#define do_log_s(level, format, ...) \ + blog(level, "[obs-webrtc] [whip_output: '%s'] " format, \ + obs_output_get_name(whipOutput->output), ##__VA_ARGS__) + +class WHIPOutput { +public: + WHIPOutput(obs_data_t *settings, obs_output_t *output); + ~WHIPOutput(); + + bool Start(); + void Stop(bool signal = true); + void Data(struct encoder_packet *packet); + + inline size_t GetTotalBytes() { return total_bytes_sent; } + + inline int GetConnectTime() { return connect_time_ms; } + +private: + void ConfigureAudioTrack(std::string media_stream_id, + std::string cname); + void ConfigureVideoTrack(std::string media_stream_id, + std::string cname); + bool Setup(); + bool Connect(); + void StartThread(); + + void SendDelete(); + void StopThread(bool signal); + + void Send(void *data, uintptr_t size, uint64_t duration, int track); + + obs_output_t *output; + + std::string endpoint_url; + std::string bearer_token; + std::string resource_url; + + std::atomic running; + + std::mutex start_stop_mutex; + std::thread start_stop_thread; + + int peer_connection; + int audio_track; + int video_track; + + 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(); + +static std::string trim_string(const std::string &source) +{ + std::string ret(source); + ret.erase(0, ret.find_first_not_of(" \n\r\t")); + ret.erase(ret.find_last_not_of(" \n\r\t") + 1); + return ret; +} + +static size_t curl_writefunction(char *data, size_t size, size_t nmemb, + void *priv_data) +{ + auto read_buffer = static_cast(priv_data); + + size_t real_size = size * nmemb; + + read_buffer->append(data, real_size); + return real_size; +} + +#define LOCATION_HEADER_LENGTH 10 + +static size_t curl_headerfunction(char *data, size_t size, size_t nmemb, + void *priv_data) +{ + auto header_buffer = static_cast(priv_data); + + size_t real_size = size * nmemb; + + if (real_size < LOCATION_HEADER_LENGTH) + return real_size; + + if (!astrcmpi_n(data, "location: ", LOCATION_HEADER_LENGTH)) { + char *val = data + LOCATION_HEADER_LENGTH; + header_buffer->append(val, real_size - LOCATION_HEADER_LENGTH); + *header_buffer = trim_string(*header_buffer); + } + + return real_size; +} diff --git a/plugins/obs-webrtc/whip-service.cpp b/plugins/obs-webrtc/whip-service.cpp new file mode 100644 index 00000000000000..70842ee3a52740 --- /dev/null +++ b/plugins/obs-webrtc/whip-service.cpp @@ -0,0 +1,105 @@ +#include "whip-service.h" + +const char *audio_codecs[MAX_CODECS] = {"opus"}; +const char *video_codecs[MAX_CODECS] = {"h264"}; + +WHIPService::WHIPService(obs_data_t *settings, obs_service_t *) + : server(), bearer_token() +{ + Update(settings); +} + +void WHIPService::Update(obs_data_t *settings) +{ + server = obs_data_get_string(settings, "server"); + bearer_token = obs_data_get_string(settings, "bearer_token"); +} + +obs_properties_t *WHIPService::Properties() +{ + obs_properties_t *ppts = obs_properties_create(); + + obs_properties_add_text(ppts, "server", "URL", OBS_TEXT_DEFAULT); + obs_properties_add_text(ppts, "bearer_token", + obs_module_text("Service.BearerToken"), + OBS_TEXT_PASSWORD); + + return ppts; +} + +void WHIPService::ApplyEncoderSettings(obs_data_t *video_settings, obs_data_t *) +{ + // For now, ensure maximum compatibility with webrtc peers + if (video_settings) { + obs_data_set_int(video_settings, "bf", 0); + obs_data_set_string(video_settings, "rate_control", "CBR"); + obs_data_set_bool(video_settings, "repeat_headers", true); + } +} + +const char *WHIPService::GetConnectInfo(enum obs_service_connect_info type) +{ + switch (type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return server.c_str(); + case OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN: + return bearer_token.c_str(); + default: + return nullptr; + } +} + +bool WHIPService::CanTryToConnect() +{ + return !server.empty(); +} + +void register_whip_service() +{ + struct obs_service_info info = {}; + + info.id = "whip_custom"; + info.get_name = [](void *) -> const char * { + return obs_module_text("Service.Name"); + }; + info.create = [](obs_data_t *settings, + obs_service_t *service) -> void * { + return new WHIPService(settings, service); + }; + info.destroy = [](void *priv_data) { + delete static_cast(priv_data); + }; + info.update = [](void *priv_data, obs_data_t *settings) { + static_cast(priv_data)->Update(settings); + }; + info.get_properties = [](void *) -> obs_properties_t * { + return WHIPService::Properties(); + }; + info.get_protocol = [](void *) -> const char * { return "WHIP"; }; + info.get_url = [](void *priv_data) -> const char * { + return static_cast(priv_data)->server.c_str(); + }; + info.get_output_type = [](void *) -> const char * { + return "whip_output"; + }; + info.apply_encoder_settings = [](void *, obs_data_t *video_settings, + obs_data_t *audio_settings) { + WHIPService::ApplyEncoderSettings(video_settings, + audio_settings); + }; + info.get_supported_video_codecs = [](void *) -> const char ** { + return video_codecs; + }; + info.get_supported_audio_codecs = [](void *) -> const char ** { + return audio_codecs; + }; + info.can_try_to_connect = [](void *priv_data) -> bool { + return static_cast(priv_data)->CanTryToConnect(); + }; + info.get_connect_info = [](void *priv_data, + uint32_t type) -> const char * { + return static_cast(priv_data)->GetConnectInfo( + (enum obs_service_connect_info)type); + }; + obs_register_service(&info); +} diff --git a/plugins/obs-webrtc/whip-service.h b/plugins/obs-webrtc/whip-service.h new file mode 100644 index 00000000000000..28e6a7c6cdbdb5 --- /dev/null +++ b/plugins/obs-webrtc/whip-service.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include + +#define MAX_CODECS 3 + +struct WHIPService { + std::string server; + std::string bearer_token; + + WHIPService(obs_data_t *settings, obs_service_t *service); + + void Update(obs_data_t *settings); + static obs_properties_t *Properties(); + static void ApplyEncoderSettings(obs_data_t *video_settings, + obs_data_t *audio_settings); + bool CanTryToConnect(); + const char *GetConnectInfo(enum obs_service_connect_info type); +}; + +void register_whip_service(); diff --git a/plugins/rtmp-services/data/schema/service-schema-v5.json b/plugins/rtmp-services/data/schema/service-schema-v5.json index 983e18f5afe4ee..f3eaf271e2e227 100644 --- a/plugins/rtmp-services/data/schema/service-schema-v5.json +++ b/plugins/rtmp-services/data/schema/service-schema-v5.json @@ -25,7 +25,8 @@ "HLS", "FTL", "SRT", - "RIST" + "RIST", + "WHIP", ] }, "common": { @@ -222,7 +223,7 @@ }, { "$comment": "Require recommended output field if protocol field is not RTMP(S)", - "if": { "required": ["protocol"], "properties": { "protocol": { "pattern": "^(HLS|SRT|RIST|FTL)$" } } }, + "if": { "required": ["protocol"], "properties": { "protocol": { "pattern": "^(HLS|SRT|RIST|FTL|WHIP)$" } } }, "then": { "properties": { "recommended": { "required": ["output"] } } } } ] diff --git a/plugins/rtmp-services/rtmp-common.c b/plugins/rtmp-services/rtmp-common.c index b9c96b2c703534..5151cb695163eb 100644 --- a/plugins/rtmp-services/rtmp-common.c +++ b/plugins/rtmp-services/rtmp-common.c @@ -1086,6 +1086,8 @@ static const char *rtmp_common_get_connect_info(void *data, uint32_t type) break; } + case OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN: + return NULL; } return NULL; diff --git a/plugins/rtmp-services/rtmp-custom.c b/plugins/rtmp-services/rtmp-custom.c index a4f6cd810ce2bd..482f1ce08a027e 100644 --- a/plugins/rtmp-services/rtmp-custom.c +++ b/plugins/rtmp-services/rtmp-custom.c @@ -169,6 +169,8 @@ static const char *rtmp_custom_get_connect_info(void *data, uint32_t type) break; } + case OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN: + return NULL; } return NULL;