Skip to main content

libobs_wrapper\data\output/
mod.rs

1//! Managed OBS outputs and validated output composition.
2//!
3//! # Which level should I use?
4//!
5//! Prefer [`ObsOutputPipelineBuilder`] when creating a complete encoded output: it validates the
6//! selected output flags, codecs, protocol/service, audio mixer indices, and runtime affinity before
7//! native creation.
8//!
9//! Use [`ObsOutputComposition`] / [`ObsOutputTrait`] when an application intentionally needs to
10//! inspect or replace encoder/service attachments on an existing output. Attachment storage and
11//! lifecycle serialization are implementation details; public callers work through behavior only.
12//!
13//! Pair this module with [`crate::capabilities`] to choose concrete implementations from the OBS
14//! plugins available at runtime. For conventional recording or RTMP streaming, `libobs-simple`
15//! provides a shorter opinionated path.
16
17use libobs::obs_output;
18use std::collections::HashMap;
19use std::ptr;
20use std::sync::{Arc, Mutex, RwLock};
21
22use crate::data::object::{inner_fn_update_settings, ObsObjectTrait, ObsObjectTraitPrivate};
23use crate::data::ImmutableObsData;
24use crate::data::ObsDataPointers;
25use crate::runtime::ObsRuntime;
26use crate::unsafe_send::{Sendable, SmartPointerSendable};
27use crate::utils::{ObsDropGuard, OutputInfo};
28use crate::{impl_obs_drop, impl_signal_manager, run_with_obs};
29
30use crate::{
31    encoders::{audio::ObsAudioEncoder, video::ObsVideoEncoder},
32    services::ObsServiceRef,
33    utils::{ObsError, ObsString},
34};
35
36use super::ObsData;
37
38pub(crate) mod macros;
39mod traits;
40pub use traits::*;
41
42mod pipeline;
43pub use pipeline::*;
44
45mod replay_buffer;
46pub use replay_buffer::*;
47
48#[derive(Debug)]
49struct _ObsOutputDropGuard {
50    output: Sendable<*mut obs_output>,
51    runtime: ObsRuntime,
52}
53
54impl ObsDropGuard for _ObsOutputDropGuard {}
55
56impl_obs_drop!(_ObsOutputDropGuard, (output), move || unsafe {
57    // Safety: We are in the runtime and drop guards are always constructed from valid output pointers. Also this guard should be in an Arc, so no double free should happen.
58    libobs::obs_output_release(output.0);
59});
60
61#[derive(Debug, Clone)]
62/// A reference to an OBS output.
63///
64/// This struct represents an output in OBS, which is responsible for
65/// outputting encoded audio and video data to a destination such as:
66/// - A file (recording)
67/// - A streaming service (RTMP, etc.)
68/// - A replay buffer
69///
70/// The output is associated with video and audio encoders that convert
71/// raw media to the required format before sending/storing.
72///
73/// Attached encoders and services are retained internally for as long as the output owns them.
74/// They can be replaced or detached while the output is inactive, either individually or by
75/// applying a complete [`ObsOutputComposition`] desired state.
76pub struct ObsOutputRef {
77    /// Disconnect signals first
78    signal_manager: Arc<ObsOutputSignals>,
79
80    /// Settings for the output
81    settings: Arc<RwLock<ImmutableObsData>>,
82
83    /// Hotkey configuration data for the output
84    hotkey_data: Arc<RwLock<ImmutableObsData>>,
85
86    /// Video encoders attached to this output
87    curr_video_encoder: Arc<RwLock<Option<Arc<ObsVideoEncoder>>>>,
88
89    /// Audio encoders attached to this output
90    audio_encoders: Arc<RwLock<HashMap<usize, Arc<ObsAudioEncoder>>>>,
91
92    /// Streaming service attached to this output, if any.
93    service: Arc<RwLock<Option<Arc<ObsServiceRef>>>>,
94
95    /// Serializes output configuration with start/stop so encoder/service snapshots cannot race.
96    configuration_lock: Arc<Mutex<()>>,
97
98    /// The type identifier of this output
99    id: ObsString,
100
101    /// The unique name of this output
102    name: ObsString,
103
104    runtime: ObsRuntime,
105
106    /// Pointer to the underlying OBS output
107    output: SmartPointerSendable<*mut obs_output>,
108}
109
110impl ObsOutputTraitSealed for ObsOutputRef {
111    fn new(output: OutputInfo, runtime: ObsRuntime) -> Result<Self, ObsError> {
112        let OutputInfo {
113            id,
114            name,
115            settings,
116            hotkey_data,
117        } = output;
118
119        let settings_ptr = settings.as_ref().map(|x| x.as_ptr());
120        let hotkey_data_ptr = hotkey_data.as_ref().map(|x| x.as_ptr());
121
122        let output = run_with_obs!(
123            runtime,
124            (id, name, settings_ptr, hotkey_data_ptr),
125            move || {
126                let settings_raw_ptr = match settings_ptr {
127                    Some(s) => s.get_ptr(),
128                    None => ptr::null_mut(),
129                };
130
131                let hotkey_data_raw_ptr = match hotkey_data_ptr {
132                    Some(h) => h.get_ptr(),
133                    None => ptr::null_mut(),
134                };
135
136                let id_ptr = id.as_ptr().0;
137                let name_ptr = name.as_ptr().0;
138
139                let output = unsafe {
140                    // Safety: All pointers are valid because we are keeping them in this scope and because we are using smart pointers for ObsData
141                    libobs::obs_output_create(
142                        id_ptr,
143                        name_ptr,
144                        settings_raw_ptr,
145                        hotkey_data_raw_ptr,
146                    )
147                };
148
149                if output.is_null() {
150                    return Err(ObsError::NullPointer(None));
151                }
152
153                Ok(Sendable(output))
154            }
155        )??;
156
157        let output = SmartPointerSendable::new(
158            output.0,
159            Arc::new(_ObsOutputDropGuard {
160                output: output.clone(),
161                runtime: runtime.clone(),
162            }),
163            runtime.native_registry(),
164        );
165
166        // We are getting the settings from OBS because OBS will have updated it with default values.
167        let new_settings_ptr = run_with_obs!(runtime, (output), move || {
168            let new_settings_ptr = unsafe {
169                // Safety: At this point, the output can't be released because we are using a SmartPointer.
170                libobs::obs_output_get_settings(output.get_ptr())
171            };
172
173            if new_settings_ptr.is_null() {
174                return Err(ObsError::NullPointer(None));
175            }
176
177            Ok(Sendable(new_settings_ptr))
178        })??;
179
180        let settings = ImmutableObsData::from_raw_pointer(new_settings_ptr, runtime.clone());
181
182        // We are creating the hotkey data here because even it is null, OBS would create it nonetheless.
183        // https://github.com/obsproject/obs-studio/blob/d97e5ad820abcccf826faf897df4c7f511857cd4/libobs/obs.c#L2629
184        let hotkey_data = match hotkey_data {
185            Some(h) => h,
186            None => ImmutableObsData::new(&runtime)?,
187        };
188
189        let signal_manager = ObsOutputSignals::new(&output, runtime.clone())?;
190        Ok(Self {
191            settings: Arc::new(RwLock::new(settings)),
192            hotkey_data: Arc::new(RwLock::new(hotkey_data)),
193
194            curr_video_encoder: Arc::new(RwLock::new(None)),
195            audio_encoders: Arc::new(RwLock::new(HashMap::new())),
196            service: Arc::new(RwLock::new(None)),
197            configuration_lock: Arc::new(Mutex::new(())),
198
199            output: output.clone(),
200            id,
201            name,
202
203            runtime,
204            signal_manager: Arc::new(signal_manager),
205        })
206    }
207
208    fn video_encoder_slot(&self) -> &Arc<RwLock<Option<Arc<ObsVideoEncoder>>>> {
209        &self.curr_video_encoder
210    }
211
212    fn audio_encoder_slots(&self) -> &Arc<RwLock<HashMap<usize, Arc<ObsAudioEncoder>>>> {
213        &self.audio_encoders
214    }
215
216    fn service_slot(&self) -> &Arc<RwLock<Option<Arc<ObsServiceRef>>>> {
217        &self.service
218    }
219
220    fn configuration_lock(&self) -> &Arc<Mutex<()>> {
221        &self.configuration_lock
222    }
223}
224
225impl ObsObjectTraitPrivate for ObsOutputRef {
226    fn __internal_replace_settings(&self, settings: ImmutableObsData) -> Result<(), ObsError> {
227        self.settings
228            .write()
229            .map_err(|_| ObsError::LockError("Failed to acquire write lock on settings".into()))
230            .map(|mut settings_lock| {
231                *settings_lock = settings;
232            })
233    }
234
235    fn __internal_replace_hotkey_data(
236        &self,
237        hotkey_data: ImmutableObsData,
238    ) -> Result<(), ObsError> {
239        self.hotkey_data
240            .write()
241            .map_err(|_| ObsError::LockError("Failed to acquire write lock on hotkey data".into()))
242            .map(|mut hotkey_lock| {
243                *hotkey_lock = hotkey_data;
244            })
245    }
246}
247
248impl ObsObjectTrait for ObsOutputRef {
249    type Native = *mut libobs::obs_output;
250
251    fn __native_handle(&self) -> SmartPointerSendable<Self::Native> {
252        self.output.clone()
253    }
254
255    fn name(&self) -> ObsString {
256        self.name.clone()
257    }
258
259    fn id(&self) -> ObsString {
260        self.id.clone()
261    }
262
263    fn runtime(&self) -> &ObsRuntime {
264        &self.runtime
265    }
266
267    fn settings(&self) -> Result<ImmutableObsData, ObsError> {
268        let r = self
269            .settings
270            .read()
271            .map_err(|_| ObsError::LockError("Failed to acquire read lock on settings".into()))?;
272
273        Ok(r.clone())
274    }
275
276    fn hotkey_data(&self) -> Result<ImmutableObsData, ObsError> {
277        let r = self.hotkey_data.read().map_err(|_| {
278            ObsError::LockError("Failed to acquire read lock on hotkey data".into())
279        })?;
280
281        Ok(r.clone())
282    }
283
284    fn update_settings(&self, settings: ObsData) -> Result<(), ObsError> {
285        let _configuration = self
286            .configuration_lock
287            .lock()
288            .map_err(|e| ObsError::LockError(e.to_string()))?;
289        if self.is_active()? {
290            return Err(ObsError::OutputAlreadyActive);
291        }
292
293        inner_fn_update_settings!(self, libobs::obs_output_update, settings)
294    }
295}
296
297impl ObsOutputTrait for ObsOutputRef {
298    fn signals(&self) -> &Arc<ObsOutputSignals> {
299        &self.signal_manager
300    }
301}
302
303impl_signal_manager!(|ptr: SmartPointerSendable<*mut libobs::obs_output>| unsafe {
304    // Safety: We are using a smart pointer, so it is fine
305    libobs::obs_output_get_signal_handler(ptr.get_ptr())
306}, ObsOutputSignals for *mut libobs::obs_output, [
307    "start": {},
308    "stop": {code: crate::enums::ObsOutputStopSignal},
309    "pause": {},
310    "unpause": {},
311    "starting": {},
312    "stopping": {},
313    "activate": {},
314    "deactivate": {},
315    "reconnect": {},
316    "reconnect_success": {}
317]);