libobs_wrapper\data\output/
mod.rs

1use std::sync::{Arc, RwLock};
2use std::{ffi::CStr, ptr};
3
4use anyhow::bail;
5use getters0::Getters;
6use libobs::obs_output;
7
8use crate::enums::ObsOutputStopSignal;
9use crate::runtime::ObsRuntime;
10use crate::unsafe_send::Sendable;
11use crate::utils::{AudioEncoderInfo, OutputInfo, VideoEncoderInfo};
12use crate::{impl_obs_drop, impl_signal_manager, run_with_obs};
13
14use crate::{
15    encoders::{audio::ObsAudioEncoder, video::ObsVideoEncoder},
16    utils::{ObsError, ObsString},
17};
18
19use super::ObsData;
20
21mod replay_buffer;
22pub use replay_buffer::*;
23
24#[derive(Debug)]
25struct _ObsOutputDropGuard {
26    output: Sendable<*mut obs_output>,
27    runtime: ObsRuntime,
28}
29
30impl_obs_drop!(_ObsOutputDropGuard, (output), move || unsafe {
31    libobs::obs_output_release(output);
32});
33
34#[derive(Debug, Getters, Clone)]
35#[skip_new]
36/// A reference to an OBS output.
37///
38/// This struct represents an output in OBS, which is responsible for
39/// outputting encoded audio and video data to a destination such as:
40/// - A file (recording)
41/// - A streaming service (RTMP, etc.)
42/// - A replay buffer
43///
44/// The output is associated with video and audio encoders that convert
45/// raw media to the required format before sending/storing.
46pub struct ObsOutputRef {
47    /// Disconnect signals first
48    pub(crate) signal_manager: Arc<ObsOutputSignals>,
49
50    /// Settings for the output
51    pub(crate) settings: Arc<RwLock<Option<ObsData>>>,
52
53    /// Hotkey configuration data for the output
54    pub(crate) hotkey_data: Arc<RwLock<Option<ObsData>>>,
55
56    /// Video encoders attached to this output
57    #[get_mut]
58    pub(crate) curr_video_encoder: Arc<RwLock<Option<Arc<ObsVideoEncoder>>>>,
59
60    /// Audio encoders attached to this output
61    #[get_mut]
62    pub(crate) audio_encoders: Arc<RwLock<Option<Arc<ObsAudioEncoder>>>>,
63
64    /// Pointer to the underlying OBS output
65    #[skip_getter]
66    pub(crate) output: Sendable<*mut obs_output>,
67
68    /// The type identifier of this output
69    pub(crate) id: ObsString,
70
71    /// The unique name of this output
72    pub(crate) name: ObsString,
73
74    #[skip_getter]
75    pub(crate) runtime: ObsRuntime,
76
77    /// RAII guard that ensures proper cleanup when the output is dropped
78    #[skip_getter]
79    _drop_guard: Arc<_ObsOutputDropGuard>,
80}
81
82impl ObsOutputRef {
83    /// Creates a new output reference from the given output info and runtime.
84    ///
85    /// # Arguments
86    /// * `output` - The output information containing ID, name, and optional settings
87    /// * `runtime` - The OBS runtime instance
88    ///
89    /// # Returns
90    /// A Result containing the new ObsOutputRef or an error
91    pub(crate) fn new(output: OutputInfo, runtime: ObsRuntime) -> Result<Self, ObsError> {
92        let (output, id, name, settings, hotkey_data) = runtime
93            .run_with_obs_result(|| {
94                let OutputInfo {
95                    id,
96                    name,
97                    settings,
98                    hotkey_data,
99                } = output;
100
101                let settings_ptr = match settings.as_ref() {
102                    Some(x) => x.as_ptr(),
103                    None => Sendable(ptr::null_mut()),
104                };
105
106                let hotkey_data_ptr = match hotkey_data.as_ref() {
107                    Some(x) => x.as_ptr(),
108                    None => Sendable(ptr::null_mut()),
109                };
110
111                let output = unsafe {
112                    libobs::obs_output_create(
113                        id.as_ptr().0,
114                        name.as_ptr().0,
115                        settings_ptr.0,
116                        hotkey_data_ptr.0,
117                    )
118                };
119
120                if output.is_null() {
121                    bail!("Null pointer returned from obs_output_create");
122                }
123
124                Ok((Sendable(output), id, name, settings, hotkey_data))
125            })
126            .map_err(|e| ObsError::InvocationError(e.to_string()))?
127            .map_err(|_| ObsError::NullPointer)?;
128
129        let signal_manager = ObsOutputSignals::new(&output, runtime.clone())?;
130        Ok(Self {
131            settings: Arc::new(RwLock::new(settings)),
132            hotkey_data: Arc::new(RwLock::new(hotkey_data)),
133
134            curr_video_encoder: Arc::new(RwLock::new(None)),
135            audio_encoders: Arc::new(RwLock::new(None)),
136
137            output: output.clone(),
138            id,
139            name,
140
141            _drop_guard: Arc::new(_ObsOutputDropGuard {
142                output,
143                runtime: runtime.clone(),
144            }),
145
146            runtime,
147            signal_manager: Arc::new(signal_manager),
148        })
149    }
150
151    /// Returns the current video encoder attached to this output, if any.
152    pub fn get_current_video_encoder(&self) -> Result<Option<Arc<ObsVideoEncoder>>, ObsError> {
153        let curr = self
154            .curr_video_encoder
155            .read()
156            .map_err(|e| ObsError::LockError(e.to_string()))?;
157
158        Ok(curr.clone())
159    }
160
161    /// Creates and attaches a new audio encoder to this output.
162    ///
163    /// This method creates a new audio encoder using the provided information
164    ///  and attaches it to this output at the specified mixer index.
165    ///
166    /// # Arguments
167    /// * `info` - Information for creating the audio encoder
168    /// * `mixer_idx` - The mixer index to use (typically 0 for primary audio)
169    ///
170    /// # Returns
171    /// A Result containing an Arc-wrapped ObsAudioEncoder or an error
172    pub fn create_and_set_video_encoder(
173        &mut self,
174        info: VideoEncoderInfo,
175    ) -> Result<Arc<ObsVideoEncoder>, ObsError> {
176        // Fail early before creating the encoder if the output is active
177        if self.is_active()? {
178            return Err(ObsError::OutputAlreadyActive);
179        }
180
181        let video_enc = ObsVideoEncoder::new_from_info(info, self.runtime.clone())?;
182
183        self.set_video_encoder(video_enc.clone())?;
184        Ok(video_enc)
185    }
186
187    /// Attaches an existing video encoder to this output.
188    ///
189    /// # Arguments
190    /// * `encoder` - The video encoder to attach
191    ///
192    /// # Returns
193    /// A Result indicating success or an error
194    pub fn set_video_encoder(&mut self, encoder: Arc<ObsVideoEncoder>) -> Result<(), ObsError> {
195        if encoder.encoder.0.is_null() {
196            return Err(ObsError::NullPointer);
197        }
198
199        if self.is_active()? {
200            return Err(ObsError::OutputAlreadyActive);
201        }
202
203        let output = self.output.clone();
204        let encoder_ptr = encoder.as_ptr();
205
206        run_with_obs!(self.runtime, (output, encoder_ptr), move || unsafe {
207            libobs::obs_output_set_video_encoder(output, encoder_ptr);
208        })?;
209
210        self.curr_video_encoder
211            .write()
212            .map_err(|e| ObsError::LockError(e.to_string()))?
213            .replace(encoder);
214
215        Ok(())
216    }
217
218    /// Updates the settings of this output.
219    ///
220    /// Note: This can only be done when the output is not active.
221    ///
222    /// # Arguments
223    /// * `settings` - The new settings to apply
224    ///
225    /// # Returns
226    /// A Result indicating success or an error
227    pub fn update_settings(&mut self, settings: ObsData) -> Result<(), ObsError> {
228        if self.is_active()? {
229            return Err(ObsError::OutputAlreadyActive);
230        }
231
232        let settings_ptr = settings.as_ptr();
233        let output = self.output.clone();
234
235        run_with_obs!(self.runtime, (output, settings_ptr), move || unsafe {
236            libobs::obs_output_update(output, settings_ptr)
237        })?;
238
239        self.settings
240            .write()
241            .map_err(|e| ObsError::LockError(e.to_string()))?
242            .replace(settings);
243        Ok(())
244    }
245
246    /// Creates and attaches a new audio encoder to this output.
247    ///
248    /// This method creates a new audio encoder using the provided information,
249    /// sets up the audio handler, and attaches it to this output at the specified mixer index.
250    ///
251    /// # Arguments
252    /// * `info` - Information for creating the audio encoder
253    /// * `mixer_idx` - The mixer index to use (typically 0 for primary audio)
254    /// * `handler` - The audio output handler
255    ///
256    /// # Returns
257    /// A Result containing an Arc-wrapped ObsAudioEncoder or an error
258    pub fn create_and_set_audio_encoder(
259        &mut self,
260        info: AudioEncoderInfo,
261        mixer_idx: usize,
262    ) -> Result<Arc<ObsAudioEncoder>, ObsError> {
263        // Fail early before creating the encoder if the output is active
264        if self.is_active()? {
265            return Err(ObsError::OutputAlreadyActive);
266        }
267
268        let audio_enc = ObsAudioEncoder::new_from_info(info, mixer_idx, self.runtime.clone())?;
269        self.set_audio_encoder(audio_enc.clone(), mixer_idx)?;
270        Ok(audio_enc)
271    }
272
273    /// Attaches an existing audio encoder to this output at the specified mixer index.
274    ///
275    /// # Arguments
276    /// * `encoder` - The audio encoder to attach
277    /// * `mixer_idx` - The mixer index to use (typically 0 for primary audio)
278    ///
279    /// # Returns
280    /// A Result indicating success or an error
281    pub fn set_audio_encoder(
282        &mut self,
283        encoder: Arc<ObsAudioEncoder>,
284        mixer_idx: usize,
285    ) -> Result<(), ObsError> {
286        if encoder.encoder.0.is_null() {
287            return Err(ObsError::NullPointer);
288        }
289
290        if self.is_active()? {
291            return Err(ObsError::OutputAlreadyActive);
292        }
293
294        let encoder_ptr = encoder.encoder.clone();
295        let output_ptr = self.output.clone();
296        run_with_obs!(self.runtime, (output_ptr, encoder_ptr), move || unsafe {
297            libobs::obs_output_set_audio_encoder(output_ptr, encoder_ptr, mixer_idx)
298        })?;
299
300        self.audio_encoders
301            .write()
302            .map_err(|e| ObsError::LockError(e.to_string()))?
303            .replace(encoder);
304
305        Ok(())
306    }
307
308    /// Starts the output.
309    ///
310    /// This begins the encoding and streaming/recording process.
311    ///
312    /// # Returns
313    /// A Result indicating success or an error (e.g., if the output is already active)
314    pub fn start(&self) -> Result<(), ObsError> {
315        if self.is_active()? {
316            return Err(ObsError::OutputAlreadyActive);
317        }
318
319        // Set the video and audio encoders before starting (similar to https://github.com/obsproject/obs-studio/blob/0b1229632063a13dfd26cf1cd9dd43431d8c68f6/frontend/utility/SimpleOutput.cpp#L552)
320        let vid_encoder_ptr = self
321            .curr_video_encoder
322            .read()
323            .map_err(|e| ObsError::LockError(e.to_string()))?
324            .as_ref()
325            .map(|enc| enc.as_ptr())
326            .unwrap_or(Sendable(ptr::null_mut()));
327
328        let audio_encoder_ptr = self
329            .audio_encoders
330            .read()
331            .map_err(|e| ObsError::LockError(e.to_string()))?
332            .as_ref()
333            .map(|enc| enc.encoder.clone())
334            .unwrap_or(Sendable(ptr::null_mut()));
335
336        let output_ptr = self.output.clone();
337        let res = run_with_obs!(
338            self.runtime,
339            (output_ptr, vid_encoder_ptr, audio_encoder_ptr),
340            move || unsafe {
341                libobs::obs_encoder_set_video(vid_encoder_ptr, libobs::obs_get_video());
342                libobs::obs_encoder_set_audio(audio_encoder_ptr, libobs::obs_get_audio());
343                libobs::obs_output_start(output_ptr)
344            }
345        )?;
346
347        if res {
348            return Ok(());
349        }
350
351        let err = run_with_obs!(self.runtime, (output_ptr), move || unsafe {
352            Sendable(libobs::obs_output_get_last_error(output_ptr))
353        })?;
354
355        let c_str = unsafe { CStr::from_ptr(err.0) };
356        let err_str = c_str.to_str().ok().map(|x| x.to_string());
357
358        Err(ObsError::OutputStartFailure(err_str))
359    }
360
361    /// This pauses or resumes the given output, and waits until the output is fully paused.
362    ///
363    /// # Arguments
364    ///
365    /// * `pause` - `true` to pause the output, `false` to resume the output.
366    ///
367    /// # Returns
368    ///
369    /// * `Ok(())` - The output was paused or resumed successfully.
370    /// * `Err(ObsError::OutputPauseFailure(Some(String)))` - The output failed to pause or resume.
371    pub fn pause(&self, pause: bool) -> Result<(), ObsError> {
372        if !self.is_active()? {
373            return Err(ObsError::OutputPauseFailure(Some(
374                "Output is not active.".to_string(),
375            )));
376        }
377
378        let output_ptr = self.output.clone();
379
380        let mut rx = if pause {
381            self.signal_manager.on_pause()?
382        } else {
383            self.signal_manager.on_unpause()?
384        };
385
386        let res = run_with_obs!(self.runtime, (output_ptr), move || unsafe {
387            libobs::obs_output_pause(output_ptr, pause)
388        })?;
389
390        if res {
391            rx.blocking_recv().map_err(|_| ObsError::NoSenderError)?;
392
393            Ok(())
394        } else {
395            let err = run_with_obs!(self.runtime, (output_ptr), move || unsafe {
396                Sendable(libobs::obs_output_get_last_error(output_ptr))
397            })?;
398
399            let c_str = unsafe { CStr::from_ptr(err.0) };
400            let err_str = c_str.to_str().ok().map(|x| x.to_string());
401
402            Err(ObsError::OutputPauseFailure(err_str))
403        }
404    }
405
406    /// Stops the output.
407    ///
408    /// This ends the encoding and streaming/recording process.
409    /// The method waits for a stop signal and returns the result.
410    ///
411    /// # Returns
412    /// A Result indicating success or an error with details about why stopping failed
413    //TODO There should be some kind of "wait" for other methods to finish, generally we don't want to have multiple different methods calling methods
414    pub fn stop(&mut self) -> Result<(), ObsError> {
415        let output_ptr = self.output.clone();
416        let output_active = run_with_obs!(self.runtime, (output_ptr), move || unsafe {
417            libobs::obs_output_active(output_ptr)
418        })?;
419
420        if !output_active {
421            return Err(ObsError::OutputStopFailure(Some(
422                "Output is not active.".to_string(),
423            )));
424        }
425
426        let mut rx = self.signal_manager.on_stop()?;
427        let mut rx_deactivate = self.signal_manager.on_deactivate()?;
428
429        run_with_obs!(self.runtime, (output_ptr), move || unsafe {
430            libobs::obs_output_stop(output_ptr)
431        })?;
432
433        let signal = rx.blocking_recv().map_err(|_| ObsError::NoSenderError)?;
434
435        log::trace!("Received stop signal: {:?}", signal);
436        if signal != ObsOutputStopSignal::Success {
437            return Err(ObsError::OutputStopFailure(Some(signal.to_string())));
438        }
439
440        rx_deactivate
441            .blocking_recv()
442            .map_err(|_| ObsError::NoSenderError)?;
443
444        Ok(())
445    }
446
447    pub fn is_active(&self) -> Result<bool, ObsError> {
448        let output_ptr = self.output.clone();
449        let output_active = run_with_obs!(self.runtime, (output_ptr), move || unsafe {
450            libobs::obs_output_active(output_ptr)
451        })?;
452
453        Ok(output_active)
454    }
455
456    pub fn as_ptr(&self) -> Sendable<*mut obs_output> {
457        self.output.clone()
458    }
459}
460
461impl_signal_manager!(|ptr| unsafe { libobs::obs_output_get_signal_handler(ptr) }, ObsOutputSignals for ObsOutputRef<*mut libobs::obs_output>, [
462    "start": {},
463    "stop": {code: crate::enums::ObsOutputStopSignal},
464    "pause": {},
465    "unpause": {},
466    "starting": {},
467    "stopping": {},
468    "activate": {},
469    "deactivate": {},
470    "reconnect": {},
471    "reconnect_success": {},
472]);