Skip to main content

libobs_wrapper\data\output/
traits.rs

1use std::{
2    collections::{HashMap, HashSet},
3    ffi::CStr,
4    fmt::Debug,
5    sync::{Arc, Mutex, RwLock},
6};
7
8use crate::{
9    data::object::ObsObjectTrait,
10    encoders::{audio::ObsAudioEncoder, video::ObsVideoEncoder},
11    enums::ObsOutputStopSignal,
12    macros::trait_with_optional_send_sync,
13    run_with_obs,
14    runtime::ObsRuntime,
15    services::ObsServiceRef,
16    utils::{AudioEncoderInfo, ObsError, OutputInfo, VideoEncoderInfo},
17};
18
19use super::ObsOutputSignals;
20
21#[derive(Clone, Debug, Default)]
22/// Desired encoder/service wiring for an OBS output.
23///
24/// Applying a composition replaces the complete managed wiring in one actor command: an
25/// omitted video encoder or service is detached, and audio mixer slots not present in
26/// `audio_encoders` are cleared. All handles are validated for runtime affinity before any
27/// native state is changed.
28pub struct ObsOutputComposition {
29    video_encoder: Option<Arc<ObsVideoEncoder>>,
30    audio_encoders: HashMap<usize, Arc<ObsAudioEncoder>>,
31    service: Option<Arc<ObsServiceRef>>,
32}
33
34impl ObsOutputComposition {
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    pub fn with_video_encoder(mut self, encoder: Arc<ObsVideoEncoder>) -> Self {
40        self.video_encoder = Some(encoder);
41        self
42    }
43
44    pub fn with_audio_encoder(mut self, mixer_idx: usize, encoder: Arc<ObsAudioEncoder>) -> Self {
45        self.audio_encoders.insert(mixer_idx, encoder);
46        self
47    }
48
49    pub fn with_service(mut self, service: Arc<ObsServiceRef>) -> Self {
50        self.service = Some(service);
51        self
52    }
53
54    pub fn without_video_encoder(mut self) -> Self {
55        self.video_encoder = None;
56        self
57    }
58
59    pub fn without_audio_encoder(mut self, mixer_idx: usize) -> Self {
60        self.audio_encoders.remove(&mixer_idx);
61        self
62    }
63
64    pub fn without_service(mut self) -> Self {
65        self.service = None;
66        self
67    }
68
69    pub fn video_encoder(&self) -> Option<&Arc<ObsVideoEncoder>> {
70        self.video_encoder.as_ref()
71    }
72
73    pub fn audio_encoders(&self) -> &HashMap<usize, Arc<ObsAudioEncoder>> {
74        &self.audio_encoders
75    }
76
77    pub fn service(&self) -> Option<&Arc<ObsServiceRef>> {
78        self.service.as_ref()
79    }
80}
81
82trait_with_optional_send_sync! {
83    pub(crate) trait ObsOutputTraitSealed: Debug {
84        /// Creates a new output reference from the given output info and runtime.
85        fn new(output: OutputInfo, runtime: ObsRuntime) -> Result<Self, ObsError>
86        where
87            Self: Sized;
88
89        fn video_encoder_slot(&self) -> &Arc<RwLock<Option<Arc<ObsVideoEncoder>>>>;
90        fn audio_encoder_slots(&self) -> &Arc<RwLock<HashMap<usize, Arc<ObsAudioEncoder>>>>;
91        fn service_slot(&self) -> &Arc<RwLock<Option<Arc<ObsServiceRef>>>>;
92        fn configuration_lock(&self) -> &Arc<Mutex<()>>;
93    }
94}
95
96#[allow(private_bounds)]
97pub trait ObsOutputTrait:
98    ObsOutputTraitSealed + ObsObjectTrait<Native = *mut libobs::obs_output_t>
99{
100    fn signals(&self) -> &Arc<ObsOutputSignals>;
101
102    #[doc(hidden)]
103    fn native_output_flags(&self) -> Result<u32, ObsError> {
104        let output_ptr = self.__native_handle();
105        let runtime = self.runtime().clone();
106        run_with_obs!(runtime, (output_ptr), move || {
107            // Safety: the managed output handle remains alive for the actor call.
108            unsafe { libobs::obs_output_get_flags(output_ptr.get_ptr()) }
109        })
110    }
111
112    /// Returns the video encoder currently attached to this output, if any.
113    fn attached_video_encoder(&self) -> Result<Option<Arc<ObsVideoEncoder>>, ObsError> {
114        let curr = self
115            .video_encoder_slot()
116            .read()
117            .map_err(|e| ObsError::LockError(e.to_string()))?;
118
119        Ok(curr.clone())
120    }
121
122    /// Returns the audio encoder attached at the requested mixer index, if any.
123    fn attached_audio_encoder(
124        &self,
125        mixer_idx: usize,
126    ) -> Result<Option<Arc<ObsAudioEncoder>>, ObsError> {
127        self.audio_encoder_slots()
128            .read()
129            .map_err(|e| ObsError::LockError(e.to_string()))
130            .map(|encoders| encoders.get(&mixer_idx).cloned())
131    }
132
133    /// Returns a snapshot of all managed audio encoder attachments keyed by mixer index.
134    fn attached_audio_encoders(&self) -> Result<HashMap<usize, Arc<ObsAudioEncoder>>, ObsError> {
135        self.audio_encoder_slots()
136            .read()
137            .map_err(|e| ObsError::LockError(e.to_string()))
138            .map(|encoders| encoders.clone())
139    }
140
141    /// Returns the streaming service attached to this output, if any.
142    fn attached_service(&self) -> Result<Option<Arc<ObsServiceRef>>, ObsError> {
143        self.service_slot()
144            .read()
145            .map_err(|e| ObsError::LockError(e.to_string()))
146            .map(|service| service.clone())
147    }
148
149    #[deprecated = "Use attached_video_encoder"]
150    fn get_current_video_encoder(&self) -> Result<Option<Arc<ObsVideoEncoder>>, ObsError> {
151        self.attached_video_encoder()
152    }
153
154    #[deprecated = "Use attached_audio_encoder"]
155    fn get_current_audio_encoder(
156        &self,
157        mixer_idx: usize,
158    ) -> Result<Option<Arc<ObsAudioEncoder>>, ObsError> {
159        self.attached_audio_encoder(mixer_idx)
160    }
161
162    #[deprecated = "Use attached_audio_encoders"]
163    fn get_current_audio_encoders(&self) -> Result<HashMap<usize, Arc<ObsAudioEncoder>>, ObsError> {
164        self.attached_audio_encoders()
165    }
166
167    #[deprecated = "Use attached_service"]
168    fn get_current_service(&self) -> Result<Option<Arc<ObsServiceRef>>, ObsError> {
169        self.attached_service()
170    }
171
172    /// Returns an owned, coherent snapshot of the currently managed encoder/service wiring.
173    fn current_composition(&self) -> Result<ObsOutputComposition, ObsError> {
174        let _configuration = self
175            .configuration_lock()
176            .lock()
177            .map_err(|e| ObsError::LockError(e.to_string()))?;
178        Ok(ObsOutputComposition {
179            video_encoder: self.attached_video_encoder()?,
180            audio_encoders: self.attached_audio_encoders()?,
181            service: self.attached_service()?,
182        })
183    }
184
185    /// Attaches a streaming service to this output. Fails while the output is active.
186    fn set_service(&self, service: Arc<ObsServiceRef>) -> Result<(), ObsError> {
187        let _configuration = self
188            .configuration_lock()
189            .lock()
190            .map_err(|e| ObsError::LockError(e.to_string()))?;
191        if self.is_active()? {
192            return Err(ObsError::OutputAlreadyActive);
193        }
194        let flags = self.native_output_flags()?;
195        ensure_output_supports_service(self, flags)?;
196        self.runtime().ensure_same_runtime(service.runtime())?;
197
198        let mut slot = self
199            .service_slot()
200            .write()
201            .map_err(|e| ObsError::LockError(e.to_string()))?;
202        let output_ptr = self.__native_handle();
203        let service_ptr = service.__native_handle();
204        let runtime = self.runtime().clone();
205        run_with_obs!(runtime, (output_ptr, service_ptr), move || {
206            // Safety: both managed handles retain their native objects for the actor call.
207            unsafe { libobs::obs_output_set_service(output_ptr.get_ptr(), service_ptr.get_ptr()) };
208        })?;
209
210        slot.replace(service);
211        Ok(())
212    }
213
214    /// Replaces the complete managed output wiring in one actor command.
215    ///
216    /// This is the preferred API when constructing an output from discovered capabilities: it
217    /// validates all runtime affinities before touching libobs, detaches omitted components, and
218    /// updates the Rust-side ownership graph only after the native calls complete.
219    fn apply_composition(&self, composition: ObsOutputComposition) -> Result<(), ObsError> {
220        let _configuration = self
221            .configuration_lock()
222            .lock()
223            .map_err(|e| ObsError::LockError(e.to_string()))?;
224        if self.is_active()? {
225            return Err(ObsError::OutputAlreadyActive);
226        }
227        let flags = self.native_output_flags()?;
228        if composition.video_encoder.is_some() {
229            ensure_output_supports_video_encoder(self, flags)?;
230        }
231        if !composition.audio_encoders.is_empty() {
232            ensure_output_supports_audio_encoder(self, flags)?;
233        }
234        if composition.service.is_some() {
235            ensure_output_supports_service(self, flags)?;
236        }
237
238        for mixer_idx in composition.audio_encoders.keys() {
239            validate_audio_mixer(*mixer_idx)?;
240        }
241        if let Some(encoder) = composition.video_encoder.as_ref() {
242            self.runtime().ensure_same_runtime(encoder.runtime())?;
243        }
244        for encoder in composition.audio_encoders.values() {
245            self.runtime().ensure_same_runtime(encoder.runtime())?;
246        }
247        if let Some(service) = composition.service.as_ref() {
248            self.runtime().ensure_same_runtime(service.runtime())?;
249        }
250
251        // Take all locks before changing native state so poisoned Rust-side state cannot leave
252        // libobs and the managed ownership graph out of sync.
253        let mut video_slot = self
254            .video_encoder_slot()
255            .write()
256            .map_err(|e| ObsError::LockError(e.to_string()))?;
257        let mut audio_slots = self
258            .audio_encoder_slots()
259            .write()
260            .map_err(|e| ObsError::LockError(e.to_string()))?;
261        let mut service_slot = self
262            .service_slot()
263            .write()
264            .map_err(|e| ObsError::LockError(e.to_string()))?;
265
266        let output_ptr = self.__native_handle();
267        let video_ptr = composition
268            .video_encoder
269            .as_ref()
270            .map(|encoder| encoder.__native_handle());
271        let service_ptr = composition
272            .service
273            .as_ref()
274            .map(|service| service.__native_handle());
275        let audio_ptrs = composition
276            .audio_encoders
277            .iter()
278            .map(|(mixer_idx, encoder)| (*mixer_idx, encoder.__native_handle()))
279            .collect::<HashMap<_, _>>();
280        let mixer_indices = audio_slots
281            .keys()
282            .copied()
283            .chain(audio_ptrs.keys().copied())
284            .collect::<HashSet<_>>();
285        let runtime = self.runtime().clone();
286
287        run_with_obs!(
288            runtime,
289            (
290                output_ptr,
291                video_ptr,
292                service_ptr,
293                audio_ptrs,
294                mixer_indices,
295                flags
296            ),
297            move || unsafe {
298                // Safety: every non-null pointer is retained by a managed handle captured for the
299                // actor call. Null is the documented libobs detach value for these setters.
300                // Only call setters supported by the concrete output: libobs warns (and third-party
301                // plugins may do worse) when service/encoder setters are used on the wrong category.
302                if output_supports_video_encoder(flags) {
303                    libobs::obs_output_set_video_encoder(
304                        output_ptr.get_ptr(),
305                        video_ptr
306                            .as_ref()
307                            .map_or(std::ptr::null_mut(), |encoder| encoder.get_ptr()),
308                    );
309                }
310                if output_supports_audio_encoder(flags) {
311                    for mixer_idx in mixer_indices {
312                        libobs::obs_output_set_audio_encoder(
313                            output_ptr.get_ptr(),
314                            audio_ptrs
315                                .get(&mixer_idx)
316                                .map_or(std::ptr::null_mut(), |encoder| encoder.get_ptr()),
317                            mixer_idx,
318                        );
319                    }
320                }
321                if output_supports_service(flags) {
322                    libobs::obs_output_set_service(
323                        output_ptr.get_ptr(),
324                        service_ptr
325                            .as_ref()
326                            .map_or(std::ptr::null_mut(), |service| service.get_ptr()),
327                    );
328                }
329            }
330        )?;
331
332        *video_slot = composition.video_encoder;
333        *audio_slots = composition.audio_encoders;
334        *service_slot = composition.service;
335        Ok(())
336    }
337
338    /// Detaches the current video encoder while the output is inactive.
339    fn clear_video_encoder(&self) -> Result<(), ObsError> {
340        let _configuration = self
341            .configuration_lock()
342            .lock()
343            .map_err(|e| ObsError::LockError(e.to_string()))?;
344        if self.is_active()? {
345            return Err(ObsError::OutputAlreadyActive);
346        }
347        let flags = self.native_output_flags()?;
348        let output_ptr = self.__native_handle();
349        let runtime = self.runtime().clone();
350        let mut slot = self
351            .video_encoder_slot()
352            .write()
353            .map_err(|e| ObsError::LockError(e.to_string()))?;
354        if output_supports_video_encoder(flags) {
355            run_with_obs!(runtime, (output_ptr), move || unsafe {
356                // Safety: the managed output handle remains valid for the actor call; null detaches.
357                libobs::obs_output_set_video_encoder(output_ptr.get_ptr(), std::ptr::null_mut());
358            })?;
359        }
360        *slot = None;
361        Ok(())
362    }
363
364    /// Detaches the audio encoder at `mixer_idx` while the output is inactive.
365    fn clear_audio_encoder(&self, mixer_idx: usize) -> Result<(), ObsError> {
366        let _configuration = self
367            .configuration_lock()
368            .lock()
369            .map_err(|e| ObsError::LockError(e.to_string()))?;
370        validate_audio_mixer(mixer_idx)?;
371        if self.is_active()? {
372            return Err(ObsError::OutputAlreadyActive);
373        }
374        let flags = self.native_output_flags()?;
375        let output_ptr = self.__native_handle();
376        let runtime = self.runtime().clone();
377        let mut slots = self
378            .audio_encoder_slots()
379            .write()
380            .map_err(|e| ObsError::LockError(e.to_string()))?;
381        if output_supports_audio_encoder(flags) {
382            run_with_obs!(runtime, (output_ptr), move || unsafe {
383                // Safety: the managed output handle remains valid for the actor call; null detaches.
384                libobs::obs_output_set_audio_encoder(
385                    output_ptr.get_ptr(),
386                    std::ptr::null_mut(),
387                    mixer_idx,
388                );
389            })?;
390        }
391        slots.remove(&mixer_idx);
392        Ok(())
393    }
394
395    /// Detaches the current streaming service while the output is inactive.
396    fn clear_service(&self) -> Result<(), ObsError> {
397        let _configuration = self
398            .configuration_lock()
399            .lock()
400            .map_err(|e| ObsError::LockError(e.to_string()))?;
401        if self.is_active()? {
402            return Err(ObsError::OutputAlreadyActive);
403        }
404        let flags = self.native_output_flags()?;
405        let output_ptr = self.__native_handle();
406        let runtime = self.runtime().clone();
407        let mut slot = self
408            .service_slot()
409            .write()
410            .map_err(|e| ObsError::LockError(e.to_string()))?;
411        if output_supports_service(flags) {
412            run_with_obs!(runtime, (output_ptr), move || unsafe {
413                // Safety: the managed output handle remains valid for the actor call; null detaches.
414                libobs::obs_output_set_service(output_ptr.get_ptr(), std::ptr::null_mut());
415            })?;
416        }
417        *slot = None;
418        Ok(())
419    }
420
421    /// Creates and attaches a new video encoder to this output.
422    ///
423    /// Fails if the output is active.
424    fn create_and_set_video_encoder(
425        &self,
426        info: VideoEncoderInfo,
427    ) -> Result<Arc<ObsVideoEncoder>, ObsError> {
428        if self.is_active()? {
429            return Err(ObsError::OutputAlreadyActive);
430        }
431        ensure_output_supports_video_encoder(self, self.native_output_flags()?)?;
432
433        let video_enc = ObsVideoEncoder::new_from_info(info, self.runtime().clone())?;
434
435        self.set_video_encoder(video_enc.clone())?;
436        Ok(video_enc)
437    }
438
439    /// Attaches an existing video encoder to this output.
440    ///
441    /// Fails if the output is active.
442    fn set_video_encoder(&self, encoder: Arc<ObsVideoEncoder>) -> Result<(), ObsError> {
443        let _configuration = self
444            .configuration_lock()
445            .lock()
446            .map_err(|e| ObsError::LockError(e.to_string()))?;
447        if self.is_active()? {
448            return Err(ObsError::OutputAlreadyActive);
449        }
450        ensure_output_supports_video_encoder(self, self.native_output_flags()?)?;
451        self.runtime().ensure_same_runtime(encoder.runtime())?;
452
453        let mut slot = self
454            .video_encoder_slot()
455            .write()
456            .map_err(|e| ObsError::LockError(e.to_string()))?;
457        let output_ptr = self.__native_handle();
458        let encoder_ptr = encoder.__native_handle();
459        let runtime = self.runtime().clone();
460
461        run_with_obs!(runtime, (output_ptr, encoder_ptr), move || {
462            unsafe {
463                // Safety: This is safe because we are only using smart pointers.
464                libobs::obs_output_set_video_encoder(output_ptr.get_ptr(), encoder_ptr.get_ptr());
465            }
466        })?;
467
468        slot.replace(encoder);
469
470        Ok(())
471    }
472
473    /// Creates and attaches a new audio encoder for the given mixer index. Fails if output active.
474    fn create_and_set_audio_encoder(
475        &self,
476        info: AudioEncoderInfo,
477        mixer_idx: usize,
478    ) -> Result<Arc<ObsAudioEncoder>, ObsError> {
479        validate_audio_mixer(mixer_idx)?;
480        if self.is_active()? {
481            return Err(ObsError::OutputAlreadyActive);
482        }
483        ensure_output_supports_audio_encoder(self, self.native_output_flags()?)?;
484
485        let audio_enc = ObsAudioEncoder::new_from_info(info, mixer_idx, self.runtime().clone())?;
486        self.set_audio_encoder(audio_enc.clone(), mixer_idx)?;
487        Ok(audio_enc)
488    }
489
490    /// Attaches an existing audio encoder to this output at the mixer index.
491    ///
492    /// Fails if the output is active.
493    fn set_audio_encoder(
494        &self,
495        encoder: Arc<ObsAudioEncoder>,
496        mixer_idx: usize,
497    ) -> Result<(), ObsError> {
498        let _configuration = self
499            .configuration_lock()
500            .lock()
501            .map_err(|e| ObsError::LockError(e.to_string()))?;
502        validate_audio_mixer(mixer_idx)?;
503        if self.is_active()? {
504            return Err(ObsError::OutputAlreadyActive);
505        }
506        ensure_output_supports_audio_encoder(self, self.native_output_flags()?)?;
507
508        self.runtime().ensure_same_runtime(encoder.runtime())?;
509
510        let mut slots = self
511            .audio_encoder_slots()
512            .write()
513            .map_err(|e| ObsError::LockError(e.to_string()))?;
514        let encoder_ptr = encoder.__native_handle();
515        let output_ptr = self.__native_handle();
516        let runtime = self.runtime().clone();
517        run_with_obs!(runtime, (output_ptr, encoder_ptr), move || {
518            unsafe {
519                // Safety: This is safe because we are only using smart pointers.
520                libobs::obs_output_set_audio_encoder(
521                    output_ptr.get_ptr(),
522                    encoder_ptr.get_ptr(),
523                    mixer_idx,
524                );
525            }
526        })?;
527
528        slots.insert(mixer_idx, encoder);
529
530        Ok(())
531    }
532
533    /// Starts the output, wiring encoders to global contexts and invoking obs_output_start.
534    /// Returns an error with last OBS message when start fails.
535    fn start(&self) -> Result<(), ObsError> {
536        let _configuration = self
537            .configuration_lock()
538            .lock()
539            .map_err(|e| ObsError::LockError(e.to_string()))?;
540        if self.is_active()? {
541            return Err(ObsError::OutputAlreadyActive);
542        }
543
544        let vid_encoder_ptr = self
545            .video_encoder_slot()
546            .read()
547            .map_err(|e| ObsError::LockError(e.to_string()))?
548            .as_ref()
549            .map(|enc| enc.__native_handle());
550
551        let audio_encoder_pointers = self
552            .audio_encoder_slots()
553            .read()
554            .map_err(|e| ObsError::LockError(e.to_string()))?
555            .values()
556            .map(|enc| enc.__native_handle())
557            .collect::<Vec<_>>();
558
559        let output_ptr = self.__native_handle();
560        let runtime = self.runtime().clone();
561        let res = run_with_obs!(
562            runtime,
563            (output_ptr, vid_encoder_ptr, audio_encoder_pointers),
564            move || {
565                if let Some(vid_encoder_ptr) = vid_encoder_ptr {
566                    unsafe {
567                        // Safety: vid_encoder_ptr is valid because of SmartPointer
568                        libobs::obs_encoder_set_video(
569                            vid_encoder_ptr.get_ptr(),
570                            libobs::obs_get_video(),
571                        );
572                    }
573                }
574                for audio_encoder_ptr in audio_encoder_pointers {
575                    unsafe {
576                        // Safety: audio_encoder_ptr is valid because of SmartPointer
577                        libobs::obs_encoder_set_audio(
578                            audio_encoder_ptr.get_ptr(),
579                            libobs::obs_get_audio(),
580                        );
581                    }
582                }
583
584                unsafe {
585                    // Safety: output_ptr is valid because of SmartPointer
586                    libobs::obs_output_start(output_ptr.get_ptr())
587                }
588            }
589        )?;
590
591        if res {
592            return Ok(());
593        }
594
595        let runtime = self.runtime().clone();
596        let err = run_with_obs!(runtime, (output_ptr), move || {
597            let err = unsafe {
598                // Safety: The output pointer must be valid because of SmartPointer
599                libobs::obs_output_get_last_error(output_ptr.get_ptr())
600            };
601
602            if err.is_null() {
603                return "Unknown error".to_string();
604            }
605
606            let err = unsafe { CStr::from_ptr(err) };
607
608            let err = err.to_string_lossy().to_string();
609            err
610        })?;
611
612        Err(ObsError::OutputStartFailure(Some(err)))
613    }
614
615    fn set_paused(&self, should_pause: bool) -> Result<(), ObsError> {
616        if self.runtime().is_actor_thread() {
617            return Err(ObsError::RuntimeReentrantBlocking);
618        }
619        if !self.is_active()? {
620            return Err(ObsError::OutputPauseFailure(Some(
621                "Output is not active.".to_string(),
622            )));
623        }
624
625        let output_ptr = self.__native_handle();
626        let runtime = self.runtime().clone();
627
628        let rx = if should_pause {
629            self.signals().on_pause()?
630        } else {
631            self.signals().on_unpause()?
632        };
633
634        let res = run_with_obs!(runtime, (output_ptr), move || {
635            unsafe {
636                // Safety: output_ptr is valid because of SmartPointer
637                libobs::obs_output_pause(output_ptr.get_ptr(), should_pause)
638            }
639        })?;
640
641        if res {
642            rx.blocking_recv().map_err(|_| ObsError::NoSenderError)?;
643
644            Ok(())
645        } else {
646            let runtime = self.runtime().clone();
647            let err = run_with_obs!(runtime, (output_ptr), move || {
648                let err = unsafe {
649                    // Safety: output_ptr is valid because of SmartPointer
650                    libobs::obs_output_get_last_error(output_ptr.get_ptr())
651                };
652
653                if err.is_null() {
654                    return None;
655                }
656
657                let err = unsafe { CStr::from_ptr(err) };
658                let err = err.to_string_lossy().to_string();
659
660                Some(err)
661            })?;
662
663            Err(ObsError::OutputPauseFailure(err))
664        }
665    }
666
667    /// Pauses or resumes the output and waits for the pause/unpause signal.
668    fn pause(&self) -> Result<(), ObsError> {
669        self.set_paused(true)
670    }
671
672    fn unpause(&self) -> Result<(), ObsError> {
673        self.set_paused(false)
674    }
675
676    /// Stops the output and waits for stop and deactivate signals.
677    fn stop(&self) -> Result<(), ObsError> {
678        if self.runtime().is_actor_thread() {
679            return Err(ObsError::RuntimeReentrantBlocking);
680        }
681        let _configuration = self
682            .configuration_lock()
683            .lock()
684            .map_err(|e| ObsError::LockError(e.to_string()))?;
685        let output_ptr = self.__native_handle();
686        let runtime = self.runtime().clone();
687        let output_active = run_with_obs!(runtime, (output_ptr), move || {
688            unsafe {
689                // Safety: output_ptr is valid because of SmartPointer
690                libobs::obs_output_active(output_ptr.get_ptr())
691            }
692        })?;
693
694        if !output_active {
695            return Err(ObsError::OutputStopFailure(Some(
696                "Output is not active.".to_string(),
697            )));
698        }
699
700        let rx = self.signals().on_stop()?;
701        let rx_deactivate = self.signals().on_deactivate()?;
702
703        let runtime = self.runtime().clone();
704        run_with_obs!(runtime, (output_ptr), move || {
705            unsafe {
706                // Safety: output_ptr is valid because of SmartPointer
707                libobs::obs_output_stop(output_ptr.get_ptr())
708            }
709        })?;
710
711        let signal = rx.blocking_recv().map_err(|_| ObsError::NoSenderError)?;
712
713        log::trace!("Received stop signal: {:?}", signal);
714        if signal != ObsOutputStopSignal::Success {
715            return Err(ObsError::OutputStopFailure(Some(signal.to_string())));
716        }
717
718        rx_deactivate
719            .blocking_recv()
720            .map_err(|_| ObsError::NoSenderError)?;
721
722        Ok(())
723    }
724
725    /// Returns whether the output is currently active.
726    fn is_active(&self) -> Result<bool, ObsError> {
727        let output_ptr = self.__native_handle();
728        let runtime = self.runtime().clone();
729        let output_active = run_with_obs!(runtime, (output_ptr), move || {
730            unsafe {
731                // Safety: output_ptr is valid because of SmartPointer
732                libobs::obs_output_active(output_ptr.get_ptr())
733            }
734        })?;
735
736        Ok(output_active)
737    }
738}
739
740fn output_supports_video_encoder(flags: u32) -> bool {
741    flags & libobs::OBS_OUTPUT_ENCODED != 0 && flags & libobs::OBS_OUTPUT_VIDEO != 0
742}
743
744fn output_supports_audio_encoder(flags: u32) -> bool {
745    flags & libobs::OBS_OUTPUT_ENCODED != 0 && flags & libobs::OBS_OUTPUT_AUDIO != 0
746}
747
748fn output_supports_service(flags: u32) -> bool {
749    flags & libobs::OBS_OUTPUT_SERVICE != 0
750}
751
752fn unsupported_component<T: ObsOutputTrait + ?Sized>(output: &T, component: &str) -> ObsError {
753    ObsError::OutputPipelineUnexpectedComponent {
754        output_id: output.id().to_string(),
755        component: component.to_owned(),
756    }
757}
758
759fn ensure_output_supports_video_encoder<T: ObsOutputTrait + ?Sized>(
760    output: &T,
761    flags: u32,
762) -> Result<(), ObsError> {
763    output_supports_video_encoder(flags)
764        .then_some(())
765        .ok_or_else(|| unsupported_component(output, "a video encoder"))
766}
767
768fn ensure_output_supports_audio_encoder<T: ObsOutputTrait + ?Sized>(
769    output: &T,
770    flags: u32,
771) -> Result<(), ObsError> {
772    output_supports_audio_encoder(flags)
773        .then_some(())
774        .ok_or_else(|| unsupported_component(output, "audio encoders"))
775}
776
777fn ensure_output_supports_service<T: ObsOutputTrait + ?Sized>(
778    output: &T,
779    flags: u32,
780) -> Result<(), ObsError> {
781    output_supports_service(flags)
782        .then_some(())
783        .ok_or_else(|| unsupported_component(output, "a service"))
784}
785
786fn validate_audio_mixer(mixer_idx: usize) -> Result<(), ObsError> {
787    if mixer_idx >= libobs::MAX_AUDIO_MIXES as usize {
788        return Err(ObsError::AudioMixerIndexOutOfBounds {
789            index: mixer_idx,
790            max: (libobs::MAX_AUDIO_MIXES - 1) as usize,
791        });
792    }
793    Ok(())
794}
795
796#[cfg(test)]
797mod tests {
798    use super::validate_audio_mixer;
799
800    #[test]
801    fn audio_mixer_indices_are_bounded_before_ffi() {
802        assert!(validate_audio_mixer(0).is_ok());
803        assert!(validate_audio_mixer((libobs::MAX_AUDIO_MIXES - 1) as usize).is_ok());
804        assert!(validate_audio_mixer(libobs::MAX_AUDIO_MIXES as usize).is_err());
805    }
806}