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}