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