From 851a8c216e14617fb523951839f3bdb240e85141 Mon Sep 17 00:00:00 2001 From: Sean DuBois Date: Thu, 6 Apr 2023 21:51:15 -0400 Subject: [PATCH] obs-webrtc: Add WHIP output & service This adds a WHIP output & associated service. - Code inspiration from DDRBoxman - Implemented by Sean DuBois & tt2468 - Various fixes and contributions by pkv. Co-authored-by: tt2468 Co-authored-by: DDRBoxman Co-authored-by: pkv Signed-off-by: pkv --- CI/linux/02_build_obs.sh | 1 + plugins/CMakeLists.txt | 2 + plugins/obs-webrtc/CMakeLists.txt | 21 + plugins/obs-webrtc/cmake/legacy.cmake | 21 + plugins/obs-webrtc/cmake/macos/Info.plist.in | 28 + .../obs-webrtc/cmake/windows/obs-module.rc.in | 24 + plugins/obs-webrtc/data/locale/en-US.ini | 3 + plugins/obs-webrtc/obs-webrtc.cpp | 19 + plugins/obs-webrtc/whip-output.cpp | 487 ++++++++++++++++++ plugins/obs-webrtc/whip-output.h | 111 ++++ plugins/obs-webrtc/whip-service.cpp | 105 ++++ plugins/obs-webrtc/whip-service.h | 21 + 12 files changed, 843 insertions(+) create mode 100644 plugins/obs-webrtc/CMakeLists.txt create mode 100644 plugins/obs-webrtc/cmake/legacy.cmake create mode 100644 plugins/obs-webrtc/cmake/macos/Info.plist.in create mode 100644 plugins/obs-webrtc/cmake/windows/obs-module.rc.in create mode 100644 plugins/obs-webrtc/data/locale/en-US.ini create mode 100644 plugins/obs-webrtc/obs-webrtc.cpp create mode 100644 plugins/obs-webrtc/whip-output.cpp create mode 100644 plugins/obs-webrtc/whip-output.h create mode 100644 plugins/obs-webrtc/whip-service.cpp create mode 100644 plugins/obs-webrtc/whip-service.h diff --git a/CI/linux/02_build_obs.sh b/CI/linux/02_build_obs.sh index b5cd618fb..9f81a651c 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/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f928f772c..6013cd5a7 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 000000000..8ddd0598e --- /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 000000000..934b9c9eb --- /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 000000000..bc130c522 --- /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 000000000..ab2a46431 --- /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 000000000..617c20e2e --- /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 000000000..ebb2eb4fd --- /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 000000000..8d45b7257 --- /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 000000000..b8b10b532 --- /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 000000000..70842ee3a --- /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 000000000..28e6a7c6c --- /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();