libobs_wrapper/
runtime.rs

1//! Runtime management for safe OBS API access across threads
2//!
3//! This module provides the core thread management functionality for the libobs-wrapper.
4//! It ensures that OBS API calls are always executed on the same thread, as required by
5//! the OBS API, while still allowing application code to interact with OBS from any thread.
6//!
7//! # Thread Safety
8//!
9//! The OBS C API is not thread-safe and requires that all operations occur on the same thread.
10//! The `ObsRuntime` struct creates a dedicated thread for all OBS operations and manages
11//! message passing between application threads and the OBS thread.
12//!
13//! # Blocking APIs
14//!
15//! The runtime locking APIs:
16//! - By default all operations are synchronous
17//!
18//! # Example
19//!
20//! ```no_run
21//! use libobs_wrapper::runtime::ObsRuntime;
22//! use libobs_wrapper::utils::StartupInfo;
23//!
24//! fn example() {
25//!     // Assuming that the OBS context is already initialized
26//!
27//!     // Run an operation on the OBS thread
28//!     let runtime = context.runtime();
29
30//!     runtime.run_with_obs(|| {
31//!         // This code runs on the OBS thread
32//!         println!("Running on OBS thread");
33//!     }).unwrap();
34//! }
35//! ```
36
37#[cfg(feature = "enable_runtime")]
38use std::any;
39use std::ffi::CStr;
40use std::rc::Rc;
41use std::sync::Arc;
42use std::{ptr, thread};
43
44use crate::context::ObsContext;
45use crate::crash_handler::main_crash_handler;
46use crate::enums::{ObsLogLevel, ObsResetVideoStatus};
47use crate::logger::{extern_log_callback, internal_log_global, LOGGER};
48#[cfg(target_os = "linux")]
49use crate::run_with_obs;
50use crate::utils::initialization::{platform_specific_setup, PlatformSpecificGuard};
51use crate::utils::{ObsError, ObsModules, ObsString};
52use crate::{context::OBS_THREAD_ID, utils::StartupInfo};
53
54#[cfg(feature = "enable_runtime")]
55use crate::unsafe_send::Sendable;
56use std::fmt::Debug;
57#[cfg(feature = "enable_runtime")]
58use std::sync::atomic::{AtomicUsize, Ordering};
59#[cfg(feature = "enable_runtime")]
60use std::sync::mpsc::{channel, Sender};
61#[cfg(feature = "enable_runtime")]
62use std::sync::Mutex;
63#[cfg(feature = "enable_runtime")]
64use std::thread::JoinHandle;
65
66/// Command type for operations to perform on the OBS thread
67#[cfg(feature = "enable_runtime")]
68enum ObsCommand {
69    /// Execute a function on the OBS thread and send result back if sender is provided
70    Execute(
71        Box<dyn FnOnce() -> Box<dyn any::Any + Send> + Send>,
72        Option<oneshot::Sender<Box<dyn any::Any + Send>>>,
73    ),
74    /// Signal the OBS thread to terminate
75    Terminate,
76}
77
78/// Core runtime that manages the OBS thread
79///
80/// This struct represents the runtime environment for OBS operations.
81/// It creates and manages a dedicated thread for OBS API calls to
82/// ensure thread safety while allowing interaction from any thread.
83///
84/// # Thread Safety
85///
86/// `ObsRuntime` can be safely cloned and shared across threads. All operations
87/// are automatically dispatched to the dedicated OBS thread.
88///
89/// # Lifecycle Management
90///
91/// When the last `ObsRuntime` instance is dropped, the OBS thread is automatically
92/// shut down and all OBS resources are properly released.
93#[derive(Debug, Clone)]
94pub struct ObsRuntime {
95    #[cfg(feature = "enable_runtime")]
96    command_sender: Arc<Sender<ObsCommand>>,
97    #[cfg(feature = "enable_runtime")]
98    queued_commands: Arc<AtomicUsize>,
99    thread_id: std::thread::ThreadId,
100    _guard: Arc<_ObsRuntimeGuard>,
101
102    #[cfg(not(feature = "enable_runtime"))]
103    _platform_specific: Option<Rc<PlatformSpecificGuard>>,
104}
105
106impl ObsRuntime {
107    /// Initializes the OBS runtime.
108    ///
109    /// This function starts up OBS on a dedicated thread and prepares it for use.
110    /// It handles bootstrapping (if configured), OBS initialization, module loading,
111    /// and setup of audio/video subsystems.
112    ///
113    /// # Parameters
114    ///
115    /// * `options` - The startup configuration for OBS
116    ///
117    /// # Returns
118    ///
119    /// A `Result` containing:
120    /// - `(ObsRuntime, ObsModules, StartupInfo)`: The initialized runtime, loaded modules, and startup info.
121    /// - `ObsError`: If initialization fails.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use libobs_wrapper::runtime::{ObsRuntime, ObsRuntimeReturn};
127    /// use libobs_wrapper::utils::StartupInfo;
128    ///
129    /// fn initialize() {
130    ///     let startup_info = StartupInfo::default();
131    ///     match ObsRuntime::startup(startup_info) {
132    ///         Ok((runtime, modules, info)) => {
133    ///             // Use the initialized runtime
134    ///         },
135    ///         Err(e) => {
136    ///             // Handle initialization error
137    ///         }
138    ///     }
139    /// }
140    /// ```
141    #[allow(unused_mut)]
142    pub(crate) fn startup(
143        mut options: StartupInfo,
144    ) -> Result<(ObsRuntime, ObsModules, StartupInfo), ObsError> {
145        // Check if OBS is already running on another thread
146        let obs_id = OBS_THREAD_ID.lock().map_err(|_e| ObsError::MutexFailure)?;
147        if obs_id.is_some() {
148            return Err(ObsError::ThreadFailure);
149        }
150
151        drop(obs_id);
152
153        log::trace!("Initializing OBS context");
154        ObsRuntime::init(options)
155            .map_err(|e| ObsError::Unexpected(format!("Failed to initialize OBS runtime: {:?}", e)))
156    }
157
158    /// Internal initialization method
159    ///
160    /// Creates the OBS thread and performs core initialization.
161    #[cfg(not(feature = "enable_runtime"))]
162    fn init(info: StartupInfo) -> Result<(ObsRuntime, ObsModules, StartupInfo), ObsError> {
163        let (startup, mut modules, platform_specific) = unsafe { Self::initialize_inner(info)? };
164
165        let runtime = Self {
166            thread_id: thread::current().id(),
167            _guard: Arc::new(_ObsRuntimeGuard {}),
168            _platform_specific: platform_specific,
169        };
170
171        modules.runtime = Some(runtime.clone());
172        Ok((runtime, modules, startup))
173    }
174
175    /// Internal initialization method
176    ///
177    /// Creates the OBS thread and performs core initialization.
178    #[cfg(feature = "enable_runtime")]
179    fn init(info: StartupInfo) -> Result<(ObsRuntime, ObsModules, StartupInfo), ObsError> {
180        static RUNTIME_THREAD_NAME: &str = "libobs-wrapper-obs-runtime";
181
182        let (command_sender, command_receiver) = channel();
183        let (init_tx, init_rx) = oneshot::channel();
184        let queued_commands = Arc::new(AtomicUsize::new(0));
185
186        let queued_commands_clone = queued_commands.clone();
187        let handle = std::thread::Builder::new()
188            .name(RUNTIME_THREAD_NAME.to_string())
189            .spawn(move || {
190                log::trace!("Starting OBS thread");
191
192                let res = unsafe {
193                    // Safety: This is safe to can because we are in the dedicated OBS thread.
194                    Self::initialize_inner(info)
195                };
196
197                match res {
198                    Ok((info, modules, _platform_specific_guard)) => {
199                        log::trace!("OBS context initialized successfully");
200
201                        let e = init_tx.send(Ok((Sendable(modules), info)));
202                        if let Err(err) = e {
203                            log::error!("Failed to send initialization signal: {:?}", err);
204                        }
205
206                        // Process commands until termination
207                        while let Ok(command) = command_receiver.recv() {
208                            match command {
209                                ObsCommand::Execute(func, result_sender) => {
210                                    let result = func();
211                                    if let Some(result_sender) = result_sender {
212                                        let _ = result_sender.send(result);
213                                    }
214
215                                    queued_commands_clone.fetch_sub(1, Ordering::SeqCst);
216                                }
217                                ObsCommand::Terminate => break,
218                            }
219                        }
220
221                        let r = unsafe {
222                            // Safety: We are in the OBS thread, so it's safe to call shutdown here.
223                            Self::shutdown_inner()
224                        };
225                        if let Err(err) = r {
226                            log::error!("Failed to shut down OBS context: {:?}", err);
227                        }
228                    }
229                    Err(err) => {
230                        log::error!("Failed to initialize OBS context: {:?}", err);
231                        let _ = init_tx.send(Err(err));
232                    }
233                }
234            })
235            .map_err(|_e| ObsError::ThreadFailure)?;
236
237        log::trace!("Waiting for OBS thread to initialize");
238        // Wait for initialization to complete
239        let (mut m, info) = init_rx.recv().map_err(|_| {
240            ObsError::RuntimeChannelError("Failed to receive initialization result".to_string())
241        })??;
242
243        let thread_id = handle.thread().id();
244        let handle = Arc::new(Mutex::new(Some(handle)));
245        let command_sender = Arc::new(command_sender);
246        let runtime = Self {
247            command_sender: command_sender.clone(),
248            thread_id,
249            queued_commands,
250            _guard: Arc::new(_ObsRuntimeGuard {
251                handle,
252                command_sender,
253            }),
254        };
255
256        m.0.runtime = Some(runtime.clone());
257        Ok((runtime, m.0, info))
258    }
259
260    /// Executes an operation on the OBS thread *without* blocking. This method *will not wait* for the result.
261    ///
262    /// # Parameters
263    ///
264    /// * `operation` - A function to execute on the OBS thread
265    ///
266    /// # Returns
267    ///
268    /// A `Result` indicating whether the operation was successfully dispatched
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// use libobs_wrapper::runtime::ObsRuntime;
274    ///
275    /// async fn example(runtime: &ObsRuntime) {
276    ///     runtime.run_with_obs(|| {
277    ///         // This code runs on the OBS thread
278    ///         println!("Hello from the OBS thread!");
279    ///     }).await.unwrap();
280    /// }
281    /// ```
282    #[cfg(feature = "enable_runtime")]
283    pub fn run_with_obs_no_block<F>(&self, operation: F) -> Result<(), ObsError>
284    where
285        F: FnOnce() + Send + 'static,
286    {
287        let is_within_runtime = std::thread::current().id() == self.thread_id;
288
289        if is_within_runtime {
290            operation();
291
292            return Ok(());
293        }
294
295        let val = self.queued_commands.fetch_add(1, Ordering::SeqCst);
296        if val > 50 {
297            log::warn!("More than 50 queued commands. Try to batch them together.");
298        }
299
300        let wrapper = move || -> Box<dyn std::any::Any + Send> {
301            operation();
302            Box::new(())
303        };
304
305        self.command_sender
306            .send(ObsCommand::Execute(Box::new(wrapper), None))
307            .map_err(|_| {
308                ObsError::RuntimeChannelError("Failed to send command to OBS thread".to_string())
309            })?;
310
311        Ok(())
312    }
313
314    /// Because you have the `enable_runtime` feature disabled, this is a no-op function and will still block. This is just so the run_with_obs macro works.
315    #[cfg(not(feature = "enable_runtime"))]
316    pub fn run_with_obs_no_block<F>(&self, operation: F) -> Result<(), ObsError>
317    where
318        F: FnOnce() + 'static,
319    {
320        // We are on runtime, so it will block either way
321        self.run_with_obs_result(operation)
322    }
323
324    /// No-Op function, as you have the runtime disabled. This is just so the run_with_obs macro still works
325    #[cfg(not(feature = "enable_runtime"))]
326    pub fn run_with_obs_result<F, T>(&self, operation: F) -> Result<T, ObsError>
327    where
328        F: FnOnce() -> T,
329    {
330        let is_within_runtime = std::thread::current().id() == self.thread_id;
331        if !is_within_runtime {
332            return Err(ObsError::RuntimeOutsideThread);
333        }
334
335        Ok(operation())
336    }
337
338    /// Executes an operation on the OBS thread, waits for the call to finish and returns a result
339    ///
340    /// This method dispatches a task to the OBS thread and blocks and waits for the result.
341    ///
342    /// # Parameters
343    ///
344    /// * `operation` - A function to execute on the OBS thread
345    ///
346    /// # Returns
347    ///
348    /// A `Result` containing the value returned by the operation
349    ///
350    /// # Examples
351    ///
352    /// ```
353    /// use libobs_wrapper::runtime::ObsRuntime;
354    ///
355    /// async fn example(runtime: &ObsRuntime) {
356    ///     let version = runtime.run_with_obs_result(|| {
357    ///         // This code runs on the OBS thread
358    ///         unsafe { libobs::obs_get_version_string() }
359    ///     }).await.unwrap();
360    ///
361    ///     println!("OBS Version: {:?}", version);
362    /// }
363    /// ```
364    #[cfg(feature = "enable_runtime")]
365    pub fn run_with_obs_result<F, T>(&self, operation: F) -> Result<T, ObsError>
366    where
367        F: FnOnce() -> T + Send + 'static,
368        T: Send + 'static,
369    {
370        let is_within_runtime = std::thread::current().id() == self.thread_id;
371        if is_within_runtime {
372            let result = operation();
373            return Ok(result);
374        }
375        let (tx, rx) = oneshot::channel();
376
377        // Create a wrapper closure that boxes the result as Any
378        let wrapper = move || -> Box<dyn std::any::Any + Send> {
379            let result = operation();
380            Box::new(result)
381        };
382
383        let val = self.queued_commands.fetch_add(1, Ordering::SeqCst);
384        if val > 50 {
385            log::warn!("More than 50 queued commands. Try to batch them together.");
386        }
387
388        self.command_sender
389            .send(ObsCommand::Execute(Box::new(wrapper), Some(tx)))
390            .map_err(|_| {
391                ObsError::RuntimeChannelError("Failed to send command to OBS thread".to_string())
392            })?;
393
394        let result = rx.recv().map_err(|_| {
395            ObsError::RuntimeChannelError("OBS thread dropped the response channel".to_string())
396        })?;
397
398        // Downcast the Any type back to T
399        let res = result.downcast::<T>().map(|boxed| *boxed).map_err(|_| {
400            ObsError::RuntimeChannelError(
401                "Failed to downcast result to the expected type".to_string(),
402            )
403        })?;
404
405        Ok(res)
406    }
407
408    /// Initializes the libobs context and prepares it for recording.
409    ///
410    /// This method handles core OBS initialization including:
411    /// - Starting up the OBS core (`obs_startup`)
412    /// - Resetting video and audio subsystems
413    /// - Loading OBS modules
414    ///
415    /// # Parameters
416    ///
417    /// * `info` - The startup configuration for OBS
418    ///
419    /// # Returns
420    ///
421    /// A `Result` containing the updated startup info and loaded modules, or an error
422    ///
423    /// # Safety
424    /// This function must be called within the OBS runtime context to ensure thread safety.
425    #[allow(unknown_lints)]
426    #[allow(ensure_obs_call_in_runtime)]
427    unsafe fn initialize_inner(
428        mut info: StartupInfo,
429    ) -> Result<(StartupInfo, ObsModules, Option<Rc<PlatformSpecificGuard>>), ObsError> {
430        // Checks that there are no other threads
431        // using libobs using a static Mutex.
432        //
433        // Fun fact: this code caused a huge debate
434        // about whether AtomicBool is UB or whatever
435        // in the Rust Programming Discord server.
436        // I didn't read too closely into it because
437        // they were talking about what architecture
438        // fridges have or something.
439        //
440        // Since this function is not meant to be
441        // high-performance or called a thousand times,
442        // a Mutex is fine here.#
443        let mut mutex_value = OBS_THREAD_ID.lock().map_err(|_e| ObsError::MutexFailure)?;
444
445        // Directly checks if the value of the
446        // Mutex is false. If true, then error.
447        // We've checked already but keeping this
448        if (*mutex_value).is_some() {
449            return Err(ObsError::ThreadFailure);
450        }
451
452        // If the Mutex is None, then change
453        // it to current thread ID so that no
454        // other thread can use libobs while
455        // the current thread is using it.
456        *mutex_value = Some(thread::current().id());
457
458        // Install DLL blocklist hook here
459
460        #[cfg(windows)]
461        unsafe {
462            // Safety: We are in the OBS thread, so it's safe to call this here.
463            libobs::obs_init_win32_crash_handler();
464        }
465
466        // Set logger, load debug privileges and crash handler
467        unsafe {
468            // Safety: We are in the OBS thread, so it's safe to call this here.
469            libobs::base_set_crash_handler(Some(main_crash_handler), std::ptr::null_mut());
470        }
471
472        let native = unsafe {
473            // Safety: We are in the OBS thread and the nix_display can only be set
474            platform_specific_setup(info.nix_display.clone())?
475        };
476        unsafe {
477            // Safety: We are in the OBS thread, so it's safe to call this here.
478            libobs::base_set_log_handler(Some(extern_log_callback), std::ptr::null_mut());
479        }
480
481        let mut log_callback = LOGGER.lock().map_err(|_e| ObsError::MutexFailure)?;
482
483        *log_callback = info.logger.take().expect("Logger can never be null");
484        drop(log_callback);
485
486        // Locale will only be used internally by
487        // libobs for logging purposes, making it
488        // unnecessary to support other languages.
489        let locale_str = ObsString::new("en-US");
490        let startup_status = unsafe {
491            // Safety: All pointers are valid here.
492            libobs::obs_startup(locale_str.as_ptr().0, ptr::null(), ptr::null_mut())
493        };
494
495        let version = unsafe { libobs::obs_get_version_string() };
496        let version_cstr = unsafe { CStr::from_ptr(version) };
497        let version_str = version_cstr.to_string_lossy().into_owned();
498
499        internal_log_global(ObsLogLevel::Info, format!("OBS {}", version_str));
500
501        // Check version compatibility
502        if !ObsContext::check_version_compatibility() {
503            internal_log_global(
504                ObsLogLevel::Warning,
505                "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!".to_string(),
506            );
507            internal_log_global(
508                ObsLogLevel::Warning,
509                format!(
510                    "OBS major version mismatch: installed version is {}, but expected major version {}. Expect crashes or bugs!!",
511                    version_str,
512                    libobs::LIBOBS_API_MAJOR_VER
513                ),
514            );
515            internal_log_global(
516                ObsLogLevel::Warning,
517                "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!".to_string(),
518            );
519        }
520
521        internal_log_global(
522            ObsLogLevel::Info,
523            "---------------------------------".to_string(),
524        );
525
526        if !startup_status {
527            return Err(ObsError::Failure);
528        }
529
530        let mut obs_modules = unsafe {
531            // Safety: This is running in the OBS thread, so it's safe to call this here.
532            ObsModules::add_paths(&info.startup_paths)
533        };
534
535        // Note that audio is meant to only be reset
536        // once. See the link below for information.
537        //
538        // https://docs.obsproject.com/frontends
539        unsafe {
540            // Safety: The audio_info pointer is valid here.
541            libobs::obs_reset_audio2(info.obs_audio_info.as_ptr().0);
542        }
543
544        // Resets the video context. Note that this
545        // is similar to Self::reset_video, but it
546        // does not call that function because the
547        // ObsContext struct is not created yet,
548        // and also because there is no need to free
549        // anything tied to the OBS context.
550        let reset_video_status = num_traits::FromPrimitive::from_i32(unsafe {
551            // Safety: The video_info pointer is valid here.
552            libobs::obs_reset_video(info.obs_video_info.as_ptr())
553        });
554
555        let reset_video_status = match reset_video_status {
556            Some(x) => x,
557            None => ObsResetVideoStatus::Failure,
558        };
559
560        if reset_video_status != ObsResetVideoStatus::Success {
561            return Err(ObsError::ResetVideoFailure(reset_video_status));
562        }
563
564        let sdr_info = info.obs_video_info.get_sdr_info();
565        unsafe {
566            // Safety: These are just numbers, so it's safe to call this here. Also graphics are initialized, so we can call this.
567            libobs::obs_set_video_levels(sdr_info.sdr_white_level, sdr_info.hdr_nominal_peak_level);
568        }
569
570        unsafe {
571            obs_modules.load_modules();
572        }
573
574        internal_log_global(
575            ObsLogLevel::Info,
576            "==== Startup complete ===============================================".to_string(),
577        );
578
579        Ok((info, obs_modules, native))
580    }
581
582    /// Shuts down the OBS context and cleans up resources
583    ///
584    /// This method performs a clean shutdown of OBS, including:
585    /// - Removing sources from output channels
586    /// - Calling `obs_shutdown` to clean up OBS resources
587    /// - Removing log and crash handlers
588    /// - Checking for memory leaks
589    ///
590    /// Safety: Always run this in the OBS runtime context.
591    #[allow(unknown_lints)]
592    #[allow(ensure_obs_call_in_runtime)]
593    unsafe fn shutdown_inner() -> Result<(), ObsError> {
594        // Clean up sources
595        for i in 0..libobs::MAX_CHANNELS {
596            unsafe { libobs::obs_set_output_source(i, ptr::null_mut()) };
597        }
598
599        unsafe {
600            // Safety: We are in the OBS thread, so it's safe to call this here. Also by this time, we _should_ have dropped all OBS resources.
601            libobs::obs_shutdown()
602        }
603
604        let r = LOGGER.lock();
605        match r {
606            Ok(mut logger) => {
607                logger.log(ObsLogLevel::Info, "OBS context shutdown.".to_string());
608                let allocs = unsafe {
609                    // Safety: Can always be called because it just returns a number.
610                    libobs::bnum_allocs()
611                };
612
613                // Increasing this to 1 because of whats described below
614                let mut notice = "";
615                let level = if allocs > 1 {
616                    ObsLogLevel::Error
617                } else {
618                    notice = " (this is an issue in the OBS source code that cannot be fixed)";
619                    ObsLogLevel::Info
620                };
621                // One memory leak is expected here because OBS does not free array elements of the obs_data_path when calling obs_add_data_path
622                // even when obs_remove_data_path is called. This is a bug in OBS.
623                logger.log(
624                    level,
625                    format!("Number of memory leaks: {}{}", allocs, notice),
626                );
627
628                #[cfg(any(feature = "__test_environment", test))]
629                {
630                    assert_eq!(allocs, 1, "Memory leaks detected: {}", allocs);
631                }
632            }
633            Err(_) => {
634                println!("OBS context shutdown. (but couldn't lock logger)");
635            }
636        }
637
638        unsafe {
639            // Safety: We are in the OBS thread, so it's safe to call this here.
640            // Clean up log and crash handler
641            libobs::base_set_crash_handler(None, std::ptr::null_mut());
642            libobs::base_set_log_handler(None, std::ptr::null_mut());
643        }
644
645        let mut mutex_value = OBS_THREAD_ID.lock().map_err(|_e| ObsError::MutexFailure)?;
646
647        *mutex_value = None;
648        Ok(())
649    }
650
651    #[cfg(target_os = "linux")]
652    pub fn get_platform(&self) -> Result<crate::utils::initialization::PlatformType, ObsError> {
653        run_with_obs!(self, || {
654            let raw_platform = unsafe {
655                // Safety: This is safe to call as long as OBS is initialized.
656                libobs::obs_get_nix_platform()
657            };
658
659            match raw_platform {
660                libobs::obs_nix_platform_type_OBS_NIX_PLATFORM_X11_EGL => {
661                    crate::utils::initialization::PlatformType::X11
662                }
663                libobs::obs_nix_platform_type_OBS_NIX_PLATFORM_WAYLAND => {
664                    crate::utils::initialization::PlatformType::Wayland
665                }
666                _ => crate::utils::initialization::PlatformType::Invalid,
667            }
668        })
669    }
670}
671
672/// Guard object to ensure proper cleanup when the runtime is dropped
673///
674/// This guard ensures that when the last reference to the runtime is dropped,
675/// the OBS thread is properly terminated and all resources are cleaned up.
676#[derive(Debug)]
677pub struct _ObsRuntimeGuard {
678    /// Thread handle for the OBS thread
679    #[cfg(feature = "enable_runtime")]
680    #[cfg_attr(
681        all(
682            feature = "no_blocking_drops",
683            not(feature = "__test_environment"),
684            not(test)
685        ),
686        allow(dead_code)
687    )]
688    handle: Arc<Mutex<Option<JoinHandle<()>>>>,
689    /// Sender channel for the OBS thread
690    #[cfg(feature = "enable_runtime")]
691    command_sender: Arc<Sender<ObsCommand>>,
692}
693
694#[cfg(feature = "enable_runtime")]
695impl Drop for _ObsRuntimeGuard {
696    /// Ensures the OBS thread is properly shut down when the runtime is dropped
697    fn drop(&mut self) {
698        log::trace!("Dropping ObsRuntime and shutting down OBS thread");
699        // Theoretically the queued_commands is zero and should be increased but because
700        // we are shutting down, we don't care about that.
701        let r = self.command_sender.send(ObsCommand::Terminate);
702
703        if thread::panicking() {
704            return;
705        }
706
707        r.expect("Failed to send termination command to OBS thread");
708        #[cfg(any(
709            not(feature = "no_blocking_drops"),
710            test,
711            feature = "__test_environment"
712        ))]
713        {
714            if cfg!(feature = "enable_runtime") {
715                // Wait for the thread to finish
716                let handle = self.handle.lock();
717                if handle.is_err() {
718                    log::error!("Failed to lock OBS thread handle for shutdown");
719                    return;
720                }
721
722                let mut handle = handle.unwrap();
723                let handle = handle.take().expect("Handle can not be empty");
724
725                handle.join().expect("Failed to join OBS thread");
726            }
727        }
728    }
729}
730
731#[cfg(not(feature = "enable_runtime"))]
732impl Drop for _ObsRuntimeGuard {
733    /// Ensures the OBS thread is properly shut down when the runtime is dropped
734    fn drop(&mut self) {
735        log::trace!("Dropping ObsRuntime and shutting down OBS thread");
736        let r = unsafe { ObsRuntime::shutdown_inner() };
737
738        if thread::panicking() {
739            return;
740        }
741
742        r.unwrap();
743    }
744}