Initial commit
commit
eeaf4aa681
|
@ -0,0 +1,60 @@
|
|||
project(char-rtc-obs)
|
||||
|
||||
find_package(Corrosion QUIET)
|
||||
if(NOT Corrosion_FOUND)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
Corrosion
|
||||
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
||||
GIT_TAG v0.3.1)
|
||||
FetchContent_MakeAvailable(Corrosion)
|
||||
endif()
|
||||
|
||||
set(CHAR_RTC_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||
|
||||
corrosion_import_crate(MANIFEST_PATH rtc-backend/Cargo.toml)
|
||||
corrosion_set_env_vars(
|
||||
char-rtc-backend "CHAR_RTC_GENERATED_DIR=${CHAR_RTC_GENERATED_DIR}"
|
||||
"CHAR_RTC_OBS_VERSION=${OBS_VERSION_CANONICAL}")
|
||||
|
||||
# Force dependent crates to link against the correct deployment target
|
||||
if(OS_MACOS)
|
||||
corrosion_set_env_vars(
|
||||
char-rtc-backend
|
||||
"CFLAGS_aarch64_apple_darwin=-mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}"
|
||||
"CFLAGS_x86_64_apple_darwin=-mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}"
|
||||
)
|
||||
corrosion_add_target_rustflags(
|
||||
char-rtc-backend
|
||||
-Clink-arg=-mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
|
||||
endif()
|
||||
|
||||
add_library(char-rtc-obs MODULE)
|
||||
|
||||
target_sources(char-rtc-obs PRIVATE char-rtc.c whip-service.c whip-service.h whip-output.c whip-output.h
|
||||
"${CHAR_RTC_GENERATED_DIR}/bindings.h")
|
||||
set_source_files_properties("${CHAR_RTC_GENERATED_DIR}/bindings.h"
|
||||
PROPERTIES GENERATED TRUE)
|
||||
|
||||
target_link_libraries(char-rtc-obs OBS::libobs char-rtc-backend)
|
||||
target_include_directories(char-rtc-obs PRIVATE ${CHAR_RTC_GENERATED_DIR})
|
||||
|
||||
if(OS_WINDOWS)
|
||||
set(MODULE_DESCRIPTION "live.umm.gay module")
|
||||
configure_file("${CMAKE_SOURCE_DIR}/cmake/bundle/windows/obs-module.rc.in"
|
||||
char-rtc-obs.rc)
|
||||
target_sources(char-rtc-obs PRIVATE char-rtc-obs.rc)
|
||||
endif()
|
||||
|
||||
if(OS_MACOS)
|
||||
find_library(COREFOUNDATION CoreFoundation)
|
||||
find_library(SECURITY_FRAMEWORK Security)
|
||||
find_library(SYSTEMCONFIGURATION SystemConfiguration)
|
||||
mark_as_advanced(COREFOUNDATION SECURITY_FRAMEWOR SYSTEMCONFIGURATION)
|
||||
target_link_libraries(char-rtc-obs ${COREFOUNDATION} ${SECURITY_FRAMEWORK}
|
||||
${SYSTEMCONFIGURATION})
|
||||
endif()
|
||||
|
||||
set_target_properties(char-rtc-obs PROPERTIES FOLDER "plugins")
|
||||
|
||||
setup_plugin_target(char-rtc-obs data)
|
|
@ -0,0 +1,5 @@
|
|||
# char-rtc-obs
|
||||
|
||||
OBS plugin to stream via WebRTC (using `webrtc-rs`) to `live.umm.gay`
|
||||
|
||||
you clone this into your `obs-studio/plugins/` folder :)
|
|
@ -0,0 +1,21 @@
|
|||
#include <obs-module.h>
|
||||
|
||||
#include "whip-output.h"
|
||||
#include "whip-service.h"
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
OBS_MODULE_USE_DEFAULT_LOCALE("char-rtc-obs", "en-US")
|
||||
MODULE_EXPORT const char *obs_module_description(void)
|
||||
{
|
||||
return obs_module_text("Module.Description");
|
||||
}
|
||||
|
||||
MODULE_EXPORT bool obs_module_load()
|
||||
{
|
||||
char_rtc_install_logger();
|
||||
|
||||
register_whip_output();
|
||||
register_whip_service();
|
||||
|
||||
return true;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
Module.Description="Publish to live.umm.gay using OBS Studio"
|
||||
Output.Name="live.umm.gay Output"
|
||||
Service.Name="live.umm.gay Service"
|
||||
Service.BearerToken="Bearer Token"
|
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "char-rtc-backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
webrtc = "0.6.0"
|
||||
webrtc-mdns = "0.5.2"
|
||||
tokio = "1.16.1"
|
||||
anyhow = "1.0.45"
|
||||
bytes = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
base64 = "0.13.0"
|
||||
reqwest = { version = "0.11.7", default-features = false, features = ["json", "rustls-tls"] }
|
||||
log = { version = "0.4.17", features = ["std"] }
|
||||
link_args = "0.6.0"
|
||||
env_logger = { version = "0.10.0", default-features = false }
|
||||
if-addrs = "0.7.0"
|
||||
|
||||
[lib]
|
||||
crate-type=["staticlib"]
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.24.3"
|
|
@ -0,0 +1,26 @@
|
|||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let bindings_dir = env::var("CHAR_RTC_GENERATED_DIR").ok();
|
||||
let version = env::var("CHAR_RTC_OBS_VERSION").unwrap_or_else(|_| "0.0.0".into());
|
||||
|
||||
// Make libobs version available
|
||||
println!("cargo:rustc-env=OBS_VERSION={version}");
|
||||
|
||||
let config = cbindgen::Config {
|
||||
language: cbindgen::Language::C,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match cbindgen::generate_with_config(crate_dir, config) {
|
||||
Ok(bindings) => {
|
||||
if let Some(bindings_dir) = bindings_dir {
|
||||
let bindings_dir: PathBuf = bindings_dir.into();
|
||||
bindings.write_to_file(bindings_dir.join("bindings.h"));
|
||||
}
|
||||
}
|
||||
Err(cbindgen::Error::ParseSyntaxError { .. }) => (), // ignore in favor of cargo's syntax check
|
||||
Err(err) => panic!("{:?}", err),
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
language = "C"
|
|
@ -0,0 +1,2 @@
|
|||
mod obs_log;
|
||||
mod output;
|
|
@ -0,0 +1,7 @@
|
|||
use crate::obs_log::OBSLogger;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn char_rtc_install_logger() {
|
||||
OBSLogger::install();
|
||||
log::info!("webrtc plugin preamble initialized")
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
use crate::output::{EncodedPacket, EncodedPacketType, OutputStream, OutputStreamError};
|
||||
use anyhow::Result;
|
||||
use log::{error, info};
|
||||
use std::{
|
||||
os::raw::{c_char, c_void},
|
||||
slice,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
pub struct RTCOutput {
|
||||
stream: OutputStream,
|
||||
runtime: Runtime,
|
||||
}
|
||||
/// cbindgen:prefix-with-name
|
||||
#[repr(C)]
|
||||
pub enum RTCOutputError {
|
||||
ConnectFailed,
|
||||
NetworkError,
|
||||
}
|
||||
|
||||
impl From<OutputStreamError> for RTCOutputError {
|
||||
fn from(ose: OutputStreamError) -> Self {
|
||||
match ose {
|
||||
OutputStreamError::ConnectFailed => RTCOutputError::ConnectFailed,
|
||||
OutputStreamError::NetworkError => RTCOutputError::NetworkError,
|
||||
OutputStreamError::WriteError(_) => RTCOutputError::NetworkError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// You must call `char_rtc_output_free` on the returned value
|
||||
#[no_mangle]
|
||||
pub extern "C" fn char_rtc_output_new() -> *mut RTCOutput {
|
||||
(|| -> Result<*mut RTCOutput> {
|
||||
let runtime = tokio::runtime::Runtime::new()?;
|
||||
let stream = runtime.block_on(OutputStream::new())?;
|
||||
Ok(Box::into_raw(Box::new(RTCOutput { stream, runtime })))
|
||||
})()
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Unable to create whip output: {e:?}");
|
||||
std::ptr::null_mut::<RTCOutput>()
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn char_rtc_output_free(output: *mut RTCOutput) {
|
||||
info!("Freeing whip output");
|
||||
if !output.is_null() {
|
||||
drop(Box::from_raw(output));
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn char_rtc_output_bytes_sent(output: *const RTCOutput) -> u64 {
|
||||
let output = output.as_ref().unwrap();
|
||||
output.stream.bytes_sent()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn char_rtc_output_dropped_frames(output: *const RTCOutput) -> i32 {
|
||||
let output = output.as_ref().unwrap();
|
||||
output.stream.dropped_frames()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn char_rtc_output_congestion(output: *const RTCOutput) -> f64 {
|
||||
let output = output.as_ref().unwrap();
|
||||
output.stream.congestion()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn char_rtc_output_connect_time_ms(output: *const RTCOutput) -> i32 {
|
||||
let output = output.as_ref().unwrap();
|
||||
output.stream.connect_time().as_millis() as i32
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn char_rtc_output_connect(
|
||||
output: *const RTCOutput,
|
||||
url: *const c_char,
|
||||
bearer_token: *const c_char,
|
||||
) {
|
||||
let output = output.as_ref().unwrap();
|
||||
|
||||
let url = std::ffi::CStr::from_ptr(url).to_str().unwrap().to_owned();
|
||||
let bearer_token = if !bearer_token.is_null() {
|
||||
Some(
|
||||
std::ffi::CStr::from_ptr(bearer_token)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
output.runtime.spawn(async move {
|
||||
output.stream.connect(&url, bearer_token.as_deref()).await;
|
||||
});
|
||||
}
|
||||
|
||||
// Once closed, you cannot call `char_rtc_output_connect` again
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn char_rtc_output_close(output: *const RTCOutput) {
|
||||
let output = output.as_ref().unwrap();
|
||||
|
||||
info!("Closing whip output");
|
||||
output
|
||||
.runtime
|
||||
.block_on(async {
|
||||
info!("I'm on a thread!");
|
||||
output.stream.close().await
|
||||
})
|
||||
.unwrap_or_else(|e| error!("Failed closing whip output: {e:?}"))
|
||||
}
|
||||
|
||||
/// Write an audio or video packet to the whip output
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn char_rtc_output_write(
|
||||
output: *const RTCOutput,
|
||||
data: *const u8,
|
||||
size: usize,
|
||||
duration: u64,
|
||||
is_audio: bool,
|
||||
) -> bool {
|
||||
let output = output.as_ref().unwrap();
|
||||
|
||||
let slice: &[u8] = slice::from_raw_parts(data, size);
|
||||
let encoded_packet = EncodedPacket {
|
||||
data: slice.to_owned(),
|
||||
duration: Duration::from_micros(duration),
|
||||
typ: if is_audio {
|
||||
EncodedPacketType::Audio
|
||||
} else {
|
||||
EncodedPacketType::Video
|
||||
},
|
||||
};
|
||||
|
||||
output
|
||||
.stream
|
||||
.write(encoded_packet)
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Failed to write packets to whip output: {e:?}");
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
pub struct ErrorCallbackUserdata(*mut c_void);
|
||||
unsafe impl Send for ErrorCallbackUserdata {}
|
||||
unsafe impl Sync for ErrorCallbackUserdata {}
|
||||
|
||||
impl ErrorCallbackUserdata {
|
||||
fn as_ptr(&self) -> *mut c_void {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
type RTCOutputErrorCallback = unsafe extern "C" fn(user_data: *mut c_void, error: RTCOutputError);
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn char_rtc_output_set_error_callback(
|
||||
output: *const RTCOutput,
|
||||
cb: RTCOutputErrorCallback,
|
||||
user_data: *mut c_void,
|
||||
) {
|
||||
let output = output.as_ref().unwrap();
|
||||
|
||||
let user_data = ErrorCallbackUserdata(user_data);
|
||||
output
|
||||
.stream
|
||||
.set_error_callback(Box::new(move |error| cb(user_data.as_ptr(), error.into())))
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
mod ffi;
|
||||
mod obs_log;
|
||||
mod output;
|
||||
mod whip;
|
||||
|
||||
// Manually configure the linked runtimes for windows due to rust defaulting to msvcrt for both debug and release
|
||||
|
||||
#[cfg(all(not(debug_assertions), target_env = "msvc"))]
|
||||
link_args::windows! {
|
||||
unsafe {
|
||||
default_lib("kernel32.lib", "vcruntime.lib", "msvcrtd", "ntdll.lib", "iphlpapi.lib");
|
||||
no_default_lib("msvcrtd", "vcruntimed.lib");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(debug_assertions, target_env = "msvc"))]
|
||||
link_args::windows! {
|
||||
unsafe {
|
||||
default_lib("kernel32.lib", "vcruntimed.lib", "msvcrtd", "ntdll.lib", "iphlpapi.lib");
|
||||
no_default_lib("msvcrt", "vcruntime.lib");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
use env_logger::filter::{Builder, Filter};
|
||||
use log::{Level, Log};
|
||||
use std::{ffi::CStr, sync::atomic::AtomicBool, sync::atomic::Ordering};
|
||||
|
||||
static DEBUG_WHIP: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub(crate) fn debug_whip() -> bool {
|
||||
DEBUG_WHIP.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// cbindgen:ignore
|
||||
mod obs {
|
||||
use std::os::raw::c_char;
|
||||
use std::os::raw::c_int;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
pub(super) struct obs_cmdline_args {
|
||||
pub(super) argc: c_int,
|
||||
pub(super) argv: *const *const c_char,
|
||||
}
|
||||
|
||||
#[link(name = "libobs")]
|
||||
extern "C" {
|
||||
pub(super) fn blog(log_level: i32, format: *const c_char, ...);
|
||||
pub(super) fn obs_get_cmdline_args() -> obs_cmdline_args;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OBSLogger {
|
||||
log_filter: Filter,
|
||||
}
|
||||
|
||||
impl OBSLogger {
|
||||
pub fn install() {
|
||||
// Pull our RUST_LOG in case they are using that
|
||||
let mut log_filter: String = std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".into());
|
||||
|
||||
unsafe {
|
||||
let raw_args = obs::obs_get_cmdline_args();
|
||||
let mut args = std::slice::from_raw_parts(raw_args.argv, raw_args.argc as usize)
|
||||
.iter()
|
||||
.map(|arg| CStr::from_ptr(*arg).to_string_lossy());
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_ref() {
|
||||
"--debug-webrtc-whip" => {
|
||||
DEBUG_WHIP.store(true, Ordering::Relaxed);
|
||||
}
|
||||
"--debug-webrtc-filter" => {
|
||||
if let Some(filter) = args.next() {
|
||||
log_filter = filter.into();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut filter_builder = Builder::new();
|
||||
log::set_boxed_logger(Box::new(Self {
|
||||
log_filter: filter_builder.parse(&log_filter).build(),
|
||||
}))
|
||||
.expect("Logger already set");
|
||||
|
||||
log::set_max_level(log::LevelFilter::Trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Log for OBSLogger {
|
||||
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
||||
self.log_filter.enabled(metadata)
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
if !self.log_filter.matches(record) {
|
||||
return;
|
||||
}
|
||||
|
||||
let level = record.level();
|
||||
let log_level = match level {
|
||||
Level::Trace => (300, "[T] "),
|
||||
Level::Debug => (300, "[D] "),
|
||||
Level::Info => (300, ""),
|
||||
Level::Warn => (200, ""),
|
||||
Level::Error => (100, ""),
|
||||
};
|
||||
|
||||
let message_prefix = format!(
|
||||
"{}[{}:{}]",
|
||||
log_level.1,
|
||||
if record.target().is_empty() {
|
||||
record.module_path().unwrap_or("unk")
|
||||
} else {
|
||||
record.target()
|
||||
},
|
||||
record.line().unwrap_or(0),
|
||||
);
|
||||
|
||||
const MAX_CHUNK_SIZE: usize = 3500;
|
||||
|
||||
let message_chars = record.args().to_string().chars().collect::<Vec<char>>();
|
||||
let chunks = message_chars.chunks(MAX_CHUNK_SIZE);
|
||||
let chunks_len = chunks.len();
|
||||
for (index, chunk) in chunks.enumerate() {
|
||||
unsafe {
|
||||
let chunk = chunk.iter().cloned().collect::<String>();
|
||||
if chunks_len == 1 {
|
||||
obs::blog(
|
||||
log_level.0,
|
||||
format!("{message_prefix}: {chunk}\0").as_ptr() as *const i8,
|
||||
);
|
||||
} else {
|
||||
obs::blog(
|
||||
log_level.0,
|
||||
format!(
|
||||
"{message_prefix}: MULTIPART [{}/{chunks_len}] {chunk}\0",
|
||||
index + 1,
|
||||
)
|
||||
.as_ptr() as *const i8,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
|
@ -0,0 +1,443 @@
|
|||
use crate::whip;
|
||||
use anyhow::{anyhow, Result};
|
||||
use bytes::Bytes;
|
||||
use log::{debug, error, info, trace};
|
||||
use reqwest::Url;
|
||||
use std::{
|
||||
boxed::Box,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
thread::JoinHandle,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::{
|
||||
select,
|
||||
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
time::interval,
|
||||
};
|
||||
use webrtc::{
|
||||
api::{
|
||||
interceptor_registry::register_default_interceptors,
|
||||
media_engine::{MediaEngine, MIME_TYPE_H264, MIME_TYPE_OPUS},
|
||||
setting_engine::SettingEngine,
|
||||
APIBuilder,
|
||||
},
|
||||
ice_transport::ice_connection_state::RTCIceConnectionState,
|
||||
interceptor::registry::Registry,
|
||||
media::Sample,
|
||||
peer_connection::{
|
||||
configuration::RTCConfiguration, peer_connection_state::RTCPeerConnectionState,
|
||||
RTCPeerConnection,
|
||||
},
|
||||
rtp_transceiver::{
|
||||
rtp_codec::{RTCRtpCodecCapability, RTCRtpCodecParameters, RTPCodecType},
|
||||
rtp_transceiver_direction::RTCRtpTransceiverDirection,
|
||||
RTCPFeedback, RTCRtpTransceiverInit,
|
||||
},
|
||||
stats::StatsReportType,
|
||||
track::track_local::track_local_static_sample::TrackLocalStaticSample,
|
||||
};
|
||||
|
||||
pub type ErrorCallback = Box<dyn FnMut(OutputStreamError) + Send + Sync>;
|
||||
#[derive(Debug)]
|
||||
pub struct EncodedPacket {
|
||||
pub data: Vec<u8>,
|
||||
pub duration: Duration,
|
||||
pub typ: EncodedPacketType,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EncodedPacketType {
|
||||
Audio,
|
||||
Video,
|
||||
}
|
||||
|
||||
/// Errors for [`OutputStream`] worker thread
|
||||
#[derive(Debug)]
|
||||
pub enum OutputStreamError {
|
||||
ConnectFailed,
|
||||
NetworkError,
|
||||
WriteError(webrtc::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
Packet(EncodedPacket),
|
||||
Error(OutputStreamError),
|
||||
Close,
|
||||
}
|
||||
|
||||
pub enum WorkerResult {
|
||||
Error(OutputStreamError),
|
||||
Close,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct OutputStreamStats {
|
||||
bytes_sent: u64,
|
||||
congestion: f64,
|
||||
dropped_frames: i32,
|
||||
connect_time: Option<Instant>,
|
||||
connect_duration: Duration,
|
||||
}
|
||||
|
||||
pub struct OutputStream {
|
||||
video_track: Arc<TrackLocalStaticSample>,
|
||||
audio_track: Arc<TrackLocalStaticSample>,
|
||||
peer_connection: Arc<RTCPeerConnection>,
|
||||
stats: Arc<RwLock<OutputStreamStats>>,
|
||||
worker_tx: UnboundedSender<Message>,
|
||||
worker_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
|
||||
whip_resource: Arc<Mutex<Option<Url>>>,
|
||||
error_callback: Arc<Mutex<Option<ErrorCallback>>>,
|
||||
}
|
||||
|
||||
impl OutputStream {
|
||||
fn start_worker(&mut self, mut worker_rx: UnboundedReceiver<Message>) {
|
||||
let worker_handle = std::thread::spawn({
|
||||
let audio_track = self.audio_track.clone();
|
||||
let video_track = self.video_track.clone();
|
||||
let peer_connection = self.peer_connection.clone();
|
||||
let stats = self.stats.clone();
|
||||
let error_callback = self.error_callback.clone();
|
||||
move || {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.thread_name("webrtc_worker")
|
||||
.worker_threads(4)
|
||||
.build()
|
||||
.unwrap();
|
||||
let runtime = runtime;
|
||||
let result = runtime.block_on(
|
||||
async move {
|
||||
let mut interval = interval(Duration::from_millis(500));
|
||||
'worker_loop: loop {
|
||||
let result = select! {
|
||||
message = worker_rx.recv() => {
|
||||
match message {
|
||||
Some(Message::Packet(packet)) => {
|
||||
let sample = Sample {
|
||||
data: Bytes::from(packet.data),
|
||||
duration: packet.duration,
|
||||
..Default::default()
|
||||
};
|
||||
match (packet.typ, peer_connection.connection_state()) {
|
||||
(_, RTCPeerConnectionState::Failed | RTCPeerConnectionState::Closed) => Err(WorkerResult::Error(OutputStreamError::NetworkError)),
|
||||
(EncodedPacketType::Audio, _) => audio_track.write_sample(&sample).await.map_err(|e| WorkerResult::Error(OutputStreamError::WriteError(e))),
|
||||
(EncodedPacketType::Video, _) => video_track.write_sample(&sample).await.map_err(|e| WorkerResult::Error(OutputStreamError::WriteError(e)))
|
||||
}
|
||||
},
|
||||
Some(Message::Error(e)) => Err(WorkerResult::Error(e)),
|
||||
Some(Message::Close) | None => Err(WorkerResult::Close),
|
||||
}
|
||||
}
|
||||
_ = interval.tick() => {
|
||||
let pc_stats = peer_connection.get_stats().await;
|
||||
|
||||
let stats = &mut stats.write().unwrap();
|
||||
if let Some(StatsReportType::Transport(transport_stats)) = pc_stats.reports.get("ice_transport") {
|
||||
stats.bytes_sent = transport_stats.bytes_sent as u64;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
break 'worker_loop e;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
match result {
|
||||
WorkerResult::Close => {}
|
||||
WorkerResult::Error(e) => {
|
||||
if let Some(callback) = &mut *error_callback.lock().unwrap() {
|
||||
error!("Worker encountered error: {e:?}");
|
||||
callback(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Exiting worker thread");
|
||||
}
|
||||
});
|
||||
|
||||
*self.worker_handle.lock().unwrap() = Some(worker_handle);
|
||||
}
|
||||
|
||||
pub async fn new() -> Result<Self> {
|
||||
let video_track = Arc::new(TrackLocalStaticSample::new(
|
||||
RTCRtpCodecCapability {
|
||||
mime_type: MIME_TYPE_H264.to_owned(),
|
||||
clock_rate: 90000,
|
||||
sdp_fmtp_line: "profile-level-id=42e01f; max-fs=3600; max-mbps=108000; max-br=1400"
|
||||
.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
"video".to_owned(),
|
||||
"webrtc-rs".to_owned(),
|
||||
));
|
||||
|
||||
let audio_track = Arc::new(TrackLocalStaticSample::new(
|
||||
RTCRtpCodecCapability {
|
||||
mime_type: MIME_TYPE_OPUS.to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
"audio".to_owned(),
|
||||
"webrtc-rs".to_owned(),
|
||||
));
|
||||
|
||||
let mut m = MediaEngine::default();
|
||||
m.register_codec(
|
||||
RTCRtpCodecParameters {
|
||||
capability: RTCRtpCodecCapability {
|
||||
mime_type: MIME_TYPE_OPUS.to_owned(),
|
||||
clock_rate: 48000,
|
||||
channels: 2,
|
||||
sdp_fmtp_line: "minptime=10;useinbandfec=1".to_owned(),
|
||||
rtcp_feedback: vec![],
|
||||
},
|
||||
payload_type: 111,
|
||||
..Default::default()
|
||||
},
|
||||
RTPCodecType::Audio,
|
||||
)?;
|
||||
|
||||
m.register_codec(
|
||||
RTCRtpCodecParameters {
|
||||
capability: RTCRtpCodecCapability {
|
||||
mime_type: MIME_TYPE_H264.to_owned(),
|
||||
clock_rate: 90000,
|
||||
channels: 0,
|
||||
sdp_fmtp_line:
|
||||
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f"
|
||||
.to_owned(),
|
||||
rtcp_feedback: vec![
|
||||
RTCPFeedback {
|
||||
typ: "goog-remb".to_owned(),
|
||||
parameter: "".to_owned(),
|
||||
},
|
||||
RTCPFeedback {
|
||||
typ: "ccm".to_owned(),
|
||||
parameter: "fir".to_owned(),
|
||||
},
|
||||
RTCPFeedback {
|
||||
typ: "nack".to_owned(),
|
||||
parameter: "".to_owned(),
|
||||
},
|
||||
RTCPFeedback {
|
||||
typ: "nack".to_owned(),
|
||||
parameter: "pli".to_owned(),
|
||||
},
|
||||
],
|
||||
},
|
||||
payload_type: 125,
|
||||
..Default::default()
|
||||
},
|
||||
RTPCodecType::Video,
|
||||
)?;
|
||||
|
||||
let mut registry = Registry::new();
|
||||
registry = register_default_interceptors(registry, &mut m)?;
|
||||
|
||||
let gather_ips: Vec<_> = if_addrs::get_if_addrs()
|
||||
.unwrap_or_else(|_| Vec::new())
|
||||
.into_iter()
|
||||
.filter(|addr| {
|
||||
if !addr.is_loopback() {
|
||||
trace!(
|
||||
"Valid gather address for interface {}: {:#?}",
|
||||
addr.name,
|
||||
addr.ip()
|
||||
);
|
||||
true
|
||||
} else {
|
||||
trace!(
|
||||
"Loopback address ignored for interface {}: {:#?}",
|
||||
addr.name,
|
||||
addr.ip()
|
||||
);
|
||||
false
|
||||
}
|
||||
})
|
||||
.map(|addr| addr.ip())
|
||||
.collect();
|
||||
|
||||
let mut setting_engine = SettingEngine::default();
|
||||
|
||||
// Only attempt to limit if we found valid interfaces, otherwise do not attempt
|
||||
// to limit as we may be on a platform that doesn't expose enough information
|
||||
if !gather_ips.is_empty() {
|
||||
debug!("Gather addresses: {gather_ips:#?}");
|
||||
setting_engine.set_ip_filter(Box::new(move |ip: std::net::IpAddr| -> bool {
|
||||
gather_ips.contains(&ip)
|
||||
}));
|
||||
}
|
||||
|
||||
let api = APIBuilder::new()
|
||||
.with_media_engine(m)
|
||||
.with_interceptor_registry(registry)
|
||||
.with_setting_engine(setting_engine)
|
||||
.build();
|
||||
|
||||
// Prepare the configuration
|
||||
let config = RTCConfiguration {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let peer_connection = Arc::new(api.new_peer_connection(config).await?);
|
||||
let stats: Arc<RwLock<OutputStreamStats>> = Arc::new(RwLock::new(OutputStreamStats {
|
||||
connect_time: None,
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
let (worker_tx, worker_rx): (UnboundedSender<Message>, UnboundedReceiver<Message>) =
|
||||
mpsc::unbounded_channel();
|
||||
let error_callback: Arc<Mutex<Option<ErrorCallback>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
let mut output_stream = Self {
|
||||
audio_track,
|
||||
video_track,
|
||||
peer_connection,
|
||||
stats,
|
||||
worker_tx,
|
||||
worker_handle: Arc::new(Mutex::new(None)),
|
||||
whip_resource: Arc::new(Mutex::new(None)),
|
||||
error_callback,
|
||||
};
|
||||
|
||||
output_stream.start_worker(worker_rx);
|
||||
|
||||
Ok(output_stream)
|
||||
}
|
||||
|
||||
pub async fn connect(&self, url: &str, bearer_token: Option<&str>) {
|
||||
self.connect_internal(url, bearer_token)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Failed connecting to: {e}");
|
||||
let _ = self
|
||||
.worker_tx
|
||||
.send(Message::Error(OutputStreamError::ConnectFailed));
|
||||
})
|
||||
}
|
||||
|
||||
async fn connect_internal(&self, url: &str, bearer_token: Option<&str>) -> Result<()> {
|
||||
println!("Setting up webrtc!");
|
||||
|
||||
self.peer_connection
|
||||
.add_transceiver_from_track(
|
||||
self.video_track.clone(),
|
||||
&[RTCRtpTransceiverInit {
|
||||
direction: RTCRtpTransceiverDirection::Sendonly,
|
||||
send_encodings: Vec::new(),
|
||||
}],
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.peer_connection
|
||||
.add_transceiver_from_track(
|
||||
self.audio_track.clone(),
|
||||
&[RTCRtpTransceiverInit {
|
||||
direction: RTCRtpTransceiverDirection::Sendonly,
|
||||
send_encodings: Vec::new(),
|
||||
}],
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.peer_connection
|
||||
.on_ice_connection_state_change(Box::new(
|
||||
move |connection_state: RTCIceConnectionState| {
|
||||
info!("Connection State has changed {}", connection_state);
|
||||
if connection_state == RTCIceConnectionState::Connected {
|
||||
// on_success();
|
||||
}
|
||||
Box::pin(async {})
|
||||
},
|
||||
));
|
||||
|
||||
self.peer_connection
|
||||
.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| {
|
||||
debug!("Peer Connection State has changed: {}", s);
|
||||
|
||||
if s == RTCPeerConnectionState::Failed {
|
||||
error!("Peer connection state went to failed")
|
||||
}
|
||||
|
||||
Box::pin(async {})
|
||||
}));
|
||||
|
||||
let offer = self.peer_connection.create_offer(None).await?;
|
||||
let mut gather_complete = self.peer_connection.gathering_complete_promise().await;
|
||||
self.peer_connection.set_local_description(offer).await?;
|
||||
|
||||
// Block until gathering complete
|
||||
let _ = gather_complete.recv().await;
|
||||
|
||||
let offer = self
|
||||
.peer_connection
|
||||
.local_description()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("No local description available"))?;
|
||||
let (answer, whip_resource) = whip::offer(url, bearer_token, offer).await?;
|
||||
self.peer_connection.set_remote_description(answer).await?;
|
||||
|
||||
*self.whip_resource.lock().unwrap() = Some(whip_resource);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn close(&self) -> Result<()> {
|
||||
let close_result = self.worker_tx.send(Message::Close);
|
||||
|
||||
// Take worker handle so it's dropped
|
||||
let worker_future = self.worker_handle.lock().unwrap().take();
|
||||
// If close was a success (worker could receive messages), join on thread
|
||||
// Otherwise, worker thread is already dead or currently closing due to error callback
|
||||
if close_result.is_ok() {
|
||||
if let Some(worker_future) = worker_future {
|
||||
worker_future
|
||||
.join()
|
||||
.unwrap_or_else(|e| error!("Failed joining worker thread: {e:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
let whip_resource = self.whip_resource.lock().unwrap().take();
|
||||
if let Some(whip_resource) = whip_resource {
|
||||
whip::delete(&whip_resource).await?;
|
||||
}
|
||||
Ok(self.peer_connection.close().await?)
|
||||
}
|
||||
|
||||
pub fn bytes_sent(&self) -> u64 {
|
||||
self.stats.read().unwrap().bytes_sent
|
||||
}
|
||||
|
||||
pub fn congestion(&self) -> f64 {
|
||||
self.stats.read().unwrap().congestion
|
||||
}
|
||||
|
||||
pub fn connect_time(&self) -> Duration {
|
||||
let mut stats = self.stats.write().unwrap();
|
||||
|
||||
if let Some(connect_time) = stats.connect_time {
|
||||
stats.connect_duration = Instant::now() - connect_time;
|
||||
}
|
||||
|
||||
stats.connect_duration
|
||||
}
|
||||
|
||||
pub fn dropped_frames(&self) -> i32 {
|
||||
self.stats.read().unwrap().dropped_frames
|
||||
}
|
||||
|
||||
pub fn write(&self, packet: EncodedPacket) -> Result<()> {
|
||||
self.worker_tx
|
||||
.send(Message::Packet(packet))
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn set_error_callback(&self, callback: ErrorCallback) {
|
||||
*self.error_callback.lock().unwrap() = Some(callback);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
use anyhow::Result;
|
||||
use log::{info, warn};
|
||||
use reqwest::{
|
||||
header::{HeaderValue, AUTHORIZATION, CONTENT_TYPE, LOCATION, USER_AGENT},
|
||||
Url,
|
||||
};
|
||||
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
||||
|
||||
use crate::obs_log;
|
||||
|
||||
const OBS_VERSION: &str = env!("OBS_VERSION");
|
||||
|
||||
pub async fn offer(
|
||||
url: &str,
|
||||
bearer_token: Option<&str>,
|
||||
local_desc: RTCSessionDescription,
|
||||
) -> Result<(RTCSessionDescription, Url)> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/sdp"));
|
||||
headers.insert(
|
||||
USER_AGENT,
|
||||
HeaderValue::from_str(&format!("libobs/{OBS_VERSION}"))?,
|
||||
);
|
||||
|
||||
if let Some(bearer_token) = bearer_token {
|
||||
if !bearer_token.is_empty() {
|
||||
headers.insert(
|
||||
AUTHORIZATION,
|
||||
HeaderValue::from_str(&format!("Bearer {bearer_token}"))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if obs_log::debug_whip() {
|
||||
info!(
|
||||
"[WHIP DEBUG | CAUTION SENSITIVE INFO] Sending offer to {url}: {}",
|
||||
local_desc.sdp
|
||||
);
|
||||
}
|
||||
|
||||
let request = client.post(url).headers(headers).body(local_desc.sdp);
|
||||
|
||||
if obs_log::debug_whip() {
|
||||
info!("[WHIP DEBUG | CAUTION SENSITIVE INFO] Offer request {request:#?}");
|
||||
}
|
||||
|
||||
let response = request.send().await?;
|
||||
|
||||
if obs_log::debug_whip() {
|
||||
info!("[WHIP DEBUG | CAUTION SENSITIVE INFO] Offer response: {response:#?}");
|
||||
}
|
||||
|
||||
let mut url = response.url().to_owned();
|
||||
if let Some(location) = response.headers().get(LOCATION) {
|
||||
url.set_path(location.to_str()?);
|
||||
}
|
||||
|
||||
let body = response.text().await?;
|
||||
let sdp = RTCSessionDescription::answer(body)?;
|
||||
|
||||
if obs_log::debug_whip() {
|
||||
info!("[WHIP DEBUG | CAUTION SENSITIVE INFO] Answer SDP: {sdp:#?}");
|
||||
}
|
||||
|
||||
Ok((sdp, url))
|
||||
}
|
||||
|
||||
pub async fn delete(url: &Url) -> Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let request = client.delete(url.to_owned()).header(
|
||||
USER_AGENT,
|
||||
HeaderValue::from_str(&format!("libobs/{OBS_VERSION}"))?,
|
||||
);
|
||||
|
||||
if obs_log::debug_whip() {
|
||||
info!("[WHIP DEBUG | CAUTION SENSITIVE INFO] Delete request {request:#?}");
|
||||
}
|
||||
|
||||
let response = request.send().await?;
|
||||
|
||||
if obs_log::debug_whip() {
|
||||
info!("[WHIP DEBUG | CAUTION SENSITIVE INFO] Delete response {response:#?}");
|
||||
}
|
||||
|
||||
if !response.status().is_success() {
|
||||
warn!("Failed DELETE of whip resource: {}", response.status())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
#include "whip-output.h"
|
||||
|
||||
static void whip_output_close_unsafe(struct whip_output *output)
|
||||
{
|
||||
if (output && output->whip_output) {
|
||||
char_rtc_output_close(output->whip_output);
|
||||
char_rtc_output_free(output->whip_output);
|
||||
output->whip_output = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static const char *whip_output_getname(void *type_data)
|
||||
{
|
||||
UNUSED_PARAMETER(type_data);
|
||||
|
||||
return obs_module_text("Output.Name");
|
||||
}
|
||||
|
||||
static void *whip_output_create(obs_data_t *settings, obs_output_t *obs_output)
|
||||
{
|
||||
UNUSED_PARAMETER(settings);
|
||||
|
||||
struct whip_output *output = bzalloc(sizeof(struct whip_output));
|
||||
pthread_mutex_init_value(&output->write_mutex);
|
||||
|
||||
output->output = obs_output;
|
||||
output->whip_output = NULL;
|
||||
|
||||
// This needs to be recursive due to `obs_output_signal_stop` calling
|
||||
// `whip_output_total_bytes_sent` (also guarded by mutex)
|
||||
if (pthread_mutex_init_recursive(&output->write_mutex) != 0)
|
||||
goto fail;
|
||||
|
||||
return output;
|
||||
|
||||
fail:
|
||||
pthread_mutex_destroy(&output->write_mutex);
|
||||
bfree(output);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void whip_output_destroy(void *data)
|
||||
{
|
||||
struct whip_output *output = data;
|
||||
|
||||
pthread_mutex_lock(&output->write_mutex);
|
||||
whip_output_close_unsafe(output);
|
||||
pthread_mutex_unlock(&output->write_mutex);
|
||||
|
||||
pthread_mutex_destroy(&output->write_mutex);
|
||||
bfree(output);
|
||||
}
|
||||
|
||||
static int map_rtc_error_to_obs_error(RTCOutputError error)
|
||||
{
|
||||
switch (error) {
|
||||
case RTCOutputError_ConnectFailed:
|
||||
return OBS_OUTPUT_CONNECT_FAILED;
|
||||
case RTCOutputError_NetworkError:
|
||||
return OBS_OUTPUT_ERROR;
|
||||
default:
|
||||
blog(LOG_ERROR, "Invalid whip error code: %d", error);
|
||||
return OBS_OUTPUT_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
static void whip_output_error_callback(void *data, RTCOutputError error)
|
||||
{
|
||||
struct whip_output *output = data;
|
||||
pthread_mutex_lock(&output->write_mutex);
|
||||
if (output->whip_output) {
|
||||
whip_output_close_unsafe(output);
|
||||
obs_output_signal_stop(output->output,
|
||||
map_rtc_error_to_obs_error(error));
|
||||
}
|
||||
pthread_mutex_unlock(&output->write_mutex);
|
||||
}
|
||||
|
||||
static bool whip_output_start(void *data)
|
||||
{
|
||||
struct whip_output *output = data;
|
||||
obs_service_t *service;
|
||||
obs_data_t *service_settings;
|
||||
const char *url, *bearer_token;
|
||||
|
||||
service = obs_output_get_service(output->output);
|
||||
if (!service)
|
||||
return false;
|
||||
|
||||
if (!obs_output_can_begin_data_capture(output->output, 0))
|
||||
return false;
|
||||
|
||||
if (!obs_output_initialize_encoders(output->output, 0))
|
||||
return false;
|
||||
|
||||
output->whip_output = char_rtc_output_new();
|
||||
if (!output->whip_output) {
|
||||
blog(LOG_ERROR, "Unable to initialize whip output");
|
||||
return false;
|
||||
}
|
||||
|
||||
char_rtc_output_set_error_callback(
|
||||
output->whip_output,
|
||||
(RTCOutputErrorCallback)whip_output_error_callback, output);
|
||||
|
||||
service_settings = obs_service_get_settings(service);
|
||||
if (!service_settings)
|
||||
return false;
|
||||
|
||||
url = obs_service_get_url(service);
|
||||
bearer_token = obs_data_get_string(service_settings, "bearer_token");
|
||||
char_rtc_output_connect(output->whip_output, url, bearer_token);
|
||||
|
||||
obs_output_begin_data_capture(output->output, 0);
|
||||
|
||||
obs_data_release(service_settings);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void whip_output_stop(void *data, uint64_t ts)
|
||||
{
|
||||
UNUSED_PARAMETER(ts);
|
||||
|
||||
struct whip_output *output = data;
|
||||
|
||||
pthread_mutex_lock(&output->write_mutex);
|
||||
whip_output_close_unsafe(output);
|
||||
pthread_mutex_unlock(&output->write_mutex);
|
||||
|
||||
obs_output_signal_stop(output->output, OBS_OUTPUT_SUCCESS);
|
||||
}
|
||||
|
||||
static void whip_output_data(void *data, struct encoder_packet *packet)
|
||||
{
|
||||
struct whip_output *output = data;
|
||||
int64_t duration = 0;
|
||||
bool is_audio = false;
|
||||
|
||||
if (packet->type == OBS_ENCODER_VIDEO) {
|
||||
duration = packet->dts_usec - output->video_timestamp;
|
||||
output->video_timestamp = packet->dts_usec;
|
||||
} else if (packet->type == OBS_ENCODER_AUDIO) {
|
||||
is_audio = true;
|
||||
duration = packet->dts_usec - output->audio_timestamp;
|
||||
output->audio_timestamp = packet->dts_usec;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&output->write_mutex);
|
||||
if (output->whip_output) {
|
||||
if (!char_rtc_output_write(output->whip_output, packet->data,
|
||||
packet->size, duration, is_audio)) {
|
||||
blog(LOG_ERROR,
|
||||
"Unable to write packets to whip output");
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&output->write_mutex);
|
||||
}
|
||||
|
||||
static void whip_output_defaults(obs_data_t *defaults)
|
||||
{
|
||||
UNUSED_PARAMETER(defaults);
|
||||
}
|
||||
|
||||
static obs_properties_t *whip_output_properties(void *unused)
|
||||
{
|
||||
UNUSED_PARAMETER(unused);
|
||||
|
||||
obs_properties_t *props = obs_properties_create();
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
static uint64_t whip_output_total_bytes_sent(void *data)
|
||||
{
|
||||
struct whip_output *output = data;
|
||||
pthread_mutex_lock(&output->write_mutex);
|
||||
uint64_t bytes_sent = 0;
|
||||
if (output->whip_output)
|
||||
bytes_sent = char_rtc_output_bytes_sent(output->whip_output);
|
||||
pthread_mutex_unlock(&output->write_mutex);
|
||||
|
||||
return bytes_sent;
|
||||
}
|
||||
|
||||
static int whip_output_dropped_frames(void *data)
|
||||
{
|
||||
struct whip_output *output = data;
|
||||
pthread_mutex_lock(&output->write_mutex);
|
||||
uint32_t dropped_frames = 0;
|
||||
if (output->whip_output)
|
||||
dropped_frames =
|
||||
char_rtc_output_dropped_frames(output->whip_output);
|
||||
pthread_mutex_unlock(&output->write_mutex);
|
||||
|
||||
return dropped_frames;
|
||||
}
|
||||
|
||||
static int whip_output_connect_time_ms(void *data)
|
||||
{
|
||||
struct whip_output *output = data;
|
||||
pthread_mutex_lock(&output->write_mutex);
|
||||
uint32_t connect_time = 0;
|
||||
if (output->whip_output)
|
||||
connect_time =
|
||||
char_rtc_output_connect_time_ms(output->whip_output);
|
||||
pthread_mutex_unlock(&output->write_mutex);
|
||||
|
||||
return connect_time;
|
||||
}
|
||||
|
||||
struct obs_output_info whip_output_info = {
|
||||
.id = "whip_output",
|
||||
.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE,
|
||||
.encoded_video_codecs = "h264",
|
||||
.encoded_audio_codecs = "opus",
|
||||
.get_name = whip_output_getname,
|
||||
.create = whip_output_create,
|
||||
.destroy = whip_output_destroy,
|
||||
.start = whip_output_start,
|
||||
.stop = whip_output_stop,
|
||||
.encoded_packet = whip_output_data,
|
||||
.get_defaults = whip_output_defaults,
|
||||
.get_properties = whip_output_properties,
|
||||
.get_total_bytes = whip_output_total_bytes_sent,
|
||||
.get_connect_time_ms = whip_output_connect_time_ms,
|
||||
.get_dropped_frames = whip_output_dropped_frames,
|
||||
};
|
||||
|
||||
void register_whip_output()
|
||||
{
|
||||
obs_register_output(&whip_output_info);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#include "obs-module.h"
|
||||
#include "bindings.h"
|
||||
#include <util/threading.h>
|
||||
|
||||
struct whip_output {
|
||||
obs_output_t *output;
|
||||
|
||||
pthread_mutex_t write_mutex;
|
||||
int64_t audio_timestamp;
|
||||
int64_t video_timestamp;
|
||||
|
||||
RTCOutput *whip_output;
|
||||
};
|
||||
|
||||
void register_whip_output(void);
|
|
@ -0,0 +1,100 @@
|
|||
#include "whip-service.h"
|
||||
|
||||
struct whip_service_state {
|
||||
char *server;
|
||||
};
|
||||
|
||||
static const char *whip_service_name(void *type_data)
|
||||
{
|
||||
UNUSED_PARAMETER(type_data);
|
||||
|
||||
return obs_module_text("Service.Name");
|
||||
}
|
||||
|
||||
static void whip_service_update(void *data, obs_data_t *settings)
|
||||
{
|
||||
struct whip_service_state *service = data;
|
||||
|
||||
bfree(service->server);
|
||||
|
||||
service->server = bstrdup(obs_data_get_string(settings, "server"));
|
||||
}
|
||||
|
||||
static void whip_service_destroy(void *data)
|
||||
{
|
||||
struct whip_service_state *service = data;
|
||||
|
||||
bfree(service->server);
|
||||
bfree(service);
|
||||
}
|
||||
|
||||
static void *whip_service_create(obs_data_t *settings, obs_service_t *service)
|
||||
{
|
||||
struct whip_service_state *data =
|
||||
bzalloc(sizeof(struct whip_service_state));
|
||||
|
||||
whip_service_update(data, settings);
|
||||
|
||||
UNUSED_PARAMETER(service);
|
||||
return data;
|
||||
}
|
||||
|
||||
static const char *whip_service_url(void *data)
|
||||
{
|
||||
struct whip_service_state *service = data;
|
||||
return service->server;
|
||||
}
|
||||
|
||||
static obs_properties_t *whip_service_properties(void *data)
|
||||
{
|
||||
UNUSED_PARAMETER(data);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static const char *whip_service_get_output_type(void *data)
|
||||
{
|
||||
UNUSED_PARAMETER(data);
|
||||
|
||||
return "whip_output";
|
||||
}
|
||||
|
||||
static void whip_service_apply_encoder_settings(void *data,
|
||||
obs_data_t *video_settings,
|
||||
obs_data_t *audio_settings)
|
||||
{
|
||||
UNUSED_PARAMETER(data);
|
||||
UNUSED_PARAMETER(audio_settings);
|
||||
|
||||
// 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, "profile", "baseline");
|
||||
obs_data_set_string(video_settings, "rate_control", "CBR");
|
||||
obs_data_set_bool(video_settings, "repeat_headers", true);
|
||||
}
|
||||
}
|
||||
|
||||
struct obs_service_info whip_service_info = {
|
||||
.id = "whip_custom",
|
||||
.get_name = whip_service_name,
|
||||
.create = whip_service_create,
|
||||
.destroy = whip_service_destroy,
|
||||
.update = whip_service_update,
|
||||
.get_properties = whip_service_properties,
|
||||
.get_url = whip_service_url,
|
||||
.get_output_type = whip_service_get_output_type,
|
||||
.apply_encoder_settings = whip_service_apply_encoder_settings,
|
||||
};
|
||||
|
||||
void register_whip_service()
|
||||
{
|
||||
obs_register_service(&whip_service_info);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "obs-module.h"
|
||||
|
||||
void register_whip_service(void);
|
Loading…
Reference in New Issue