Skip to main content

libobs_wrapper/
context.rs

1//! OBS Context Management
2//!
3//! This module provides the core functionality for interacting with libobs.
4//! The primary type is [`ObsContext`], which serves as the main entry point for
5//! all OBS operations.
6//!
7//! # Overview
8//!
9//! The `ObsContext` represents an initialized OBS environment and provides methods to:
10//! - Initialize the OBS runtime
11//! - Create and manage scenes
12//! - Create and manage outputs (recording, streaming)
13//! - Access and configure video/audio settings
14//! - Work with OBS runtimes prepared at build time or explicitly provisioned before initialization
15//!
16//! # Thread Safety
17//!
18//! OBS operations must be performed on a single thread. The `ObsContext` handles
19//! this requirement by creating a dedicated thread for OBS operations and providing
20//! a thread-safe interface to interact with it.
21//!
22//! # Examples
23//!
24//! Creating a basic OBS context:
25//!
26//! ```no_run
27//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
28//! use libobs_wrapper::context::ObsContext;
29//! use libobs_wrapper::utils::StartupInfo;
30//!
31//! let info = StartupInfo::default();
32//! let context = ObsContext::new(info)?;
33//! # Ok(())
34//! # }
35//! ```
36//!
37//! For more examples refer to the [examples](https://github.com/libobs-rs/libobs-rs/tree/main/examples) directory in the repository.
38
39mod registry;
40
41use std::{
42    ffi::CStr,
43    sync::{Arc, Condvar, Mutex, RwLock},
44    thread::ThreadId,
45};
46
47#[cfg(target_os = "linux")]
48use crate::utils::initialization::PlatformType;
49use crate::{
50    data::output::{ObsOutputTrait, ObsOutputTraitSealed, ObsReplayBufferOutputRef},
51    display::{ObsDisplayCreationData, ObsDisplayRef},
52};
53use crate::{
54    data::{output::ObsOutputRef, video::ObsVideoInfo, ObsData},
55    enums::{ObsLogLevel, ObsResetVideoStatus},
56    logger::LOGGER,
57    run_with_obs,
58    runtime::ObsRuntime,
59    scenes::ObsSceneRef,
60    sources::{ObsFilterRef, ObsSourceBuilder},
61    unsafe_send::Sendable,
62    utils::{FilterInfo, ObsError, ObsModules, ObsString, OutputInfo, StartupInfo},
63};
64use getters0::Getters;
65use libobs::{audio_output, video_output};
66use registry::ObjectRegistry;
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub(crate) enum ObsRuntimeState {
70    Idle,
71    Starting,
72    Active(ThreadId),
73    ShuttingDown,
74}
75
76lazy_static::lazy_static! {
77    static ref OBS_RUNTIME_STATE: (Mutex<ObsRuntimeState>, Condvar) =
78        (Mutex::new(ObsRuntimeState::Idle), Condvar::new());
79}
80
81pub(crate) fn reserve_runtime_slot() -> Result<(), ObsError> {
82    let (lock, changed) = &*OBS_RUNTIME_STATE;
83    let mut state = lock.lock().map_err(|_| ObsError::MutexFailure)?;
84    loop {
85        match *state {
86            ObsRuntimeState::Idle => {
87                *state = ObsRuntimeState::Starting;
88                return Ok(());
89            }
90            ObsRuntimeState::ShuttingDown => {
91                state = changed.wait(state).map_err(|_| ObsError::MutexFailure)?;
92            }
93            ObsRuntimeState::Starting | ObsRuntimeState::Active(_) => {
94                return Err(ObsError::ThreadFailure);
95            }
96        }
97    }
98}
99
100pub(crate) fn cancel_runtime_start() {
101    let (lock, changed) = &*OBS_RUNTIME_STATE;
102    let mut state = match lock.lock() {
103        Ok(state) => state,
104        Err(poisoned) => poisoned.into_inner(),
105    };
106    if *state == ObsRuntimeState::Starting {
107        *state = ObsRuntimeState::Idle;
108        changed.notify_all();
109    }
110}
111
112pub(crate) fn activate_runtime_slot(thread_id: ThreadId) -> Result<(), ObsError> {
113    let (lock, _) = &*OBS_RUNTIME_STATE;
114    let mut state = lock.lock().map_err(|_| ObsError::MutexFailure)?;
115    if *state != ObsRuntimeState::Starting {
116        return Err(ObsError::ThreadFailure);
117    }
118    *state = ObsRuntimeState::Active(thread_id);
119    Ok(())
120}
121
122pub(crate) fn begin_runtime_shutdown() {
123    let (lock, _) = &*OBS_RUNTIME_STATE;
124    match lock.lock() {
125        Ok(mut state) => {
126            if matches!(*state, ObsRuntimeState::Active(_)) {
127                *state = ObsRuntimeState::ShuttingDown;
128            }
129        }
130        Err(poisoned) => {
131            let mut state = poisoned.into_inner();
132            *state = ObsRuntimeState::ShuttingDown;
133        }
134    }
135}
136
137pub(crate) fn release_runtime_slot() -> Result<(), ObsError> {
138    let (lock, changed) = &*OBS_RUNTIME_STATE;
139    let mut state = lock.lock().map_err(|_| ObsError::MutexFailure)?;
140    *state = ObsRuntimeState::Idle;
141    changed.notify_all();
142    Ok(())
143}
144
145/// Interface to the process-global OBS context.
146///
147/// Context-level native objects are owned by an internal registry. Each native handle
148/// retains the runtime independently, so correctness does not depend on Rust struct
149/// field declaration order.
150#[derive(Debug, Getters, Clone)]
151#[skip_new]
152pub struct ObsContext {
153    /// Keeps C-referenced startup configuration alive for the OBS lifetime.
154    startup_info: Arc<RwLock<StartupInfo>>,
155
156    #[skip_getter]
157    objects: Arc<ObjectRegistry>,
158
159    #[skip_getter]
160    _obs_modules: Arc<ObsModules>,
161
162    runtime: ObsRuntime,
163
164    #[cfg(target_os = "linux")]
165    glib_loop: Arc<RwLock<Option<crate::utils::linux::LinuxGlibLoop>>>,
166}
167
168impl ObsContext {
169    /// Checks if the installed OBS version matches the expected version.
170    /// Returns true if the major version matches, false otherwise.
171    pub fn check_version_compatibility() -> bool {
172        // Safety: This is fine, we are just getting a version string, which doesn't allocate any memory or have side effects.
173        unsafe {
174            #[allow(unknown_lints)]
175            #[allow(ensure_obs_call_in_runtime)]
176            let version = libobs::obs_get_version_string();
177            if version.is_null() {
178                return false;
179            }
180
181            let version_str = match CStr::from_ptr(version).to_str() {
182                Ok(s) => s,
183                Err(_) => return false,
184            };
185
186            let version_parts: Vec<&str> = version_str.split('.').collect();
187            if version_parts.len() != 3 {
188                return false;
189            }
190
191            let major = match version_parts[0].parse::<u64>() {
192                Ok(v) => v,
193                Err(_) => return false,
194            };
195
196            major == libobs::LIBOBS_API_MAJOR_VER as u64
197        }
198    }
199
200    pub fn builder() -> StartupInfo {
201        StartupInfo::new()
202    }
203
204    /// Initializes libobs on the current thread.
205    ///
206    /// Note that there can be only one ObsContext
207    /// initialized at a time. This is because
208    /// libobs is not completely thread-safe.
209    ///
210    /// Also note that this might leak a very tiny
211    /// amount of memory. As a result, it is
212    /// probably a good idea not to restart the
213    /// OBS context repeatedly over a very long
214    /// period of time. Unfortunately the memory
215    /// leak is caused by a bug in libobs itself.
216    ///
217    /// On Linux, make sure to call `ObsContext::check_version_compatibility` before
218    /// initializing the context. If that method returns false, it may be possible for the binary to crash.
219    ///
220    /// If initialization fails, an `ObsError` is returned.
221    pub fn new(info: StartupInfo) -> Result<ObsContext, ObsError> {
222        log::trace!("Getting version number...");
223
224        #[allow(unknown_lints)]
225        #[allow(ensure_obs_call_in_runtime)]
226        // Safety: This is fine, we are just getting a version number, which does not require
227        // to be on the OBS thread.
228        let version_numb = unsafe { libobs::obs_get_version() };
229        if version_numb == 0 {
230            return Err(ObsError::InvalidDll);
231        }
232
233        // Spawning runtime, I'll keep this as function for now
234        let (runtime, obs_modules, info) = ObsRuntime::startup(info)?;
235        #[cfg(target_os = "linux")]
236        let linux_opt = if info.start_glib_loop {
237            Some(crate::utils::linux::LinuxGlibLoop::new())
238        } else {
239            None
240        };
241
242        Ok(Self {
243            _obs_modules: Arc::new(obs_modules),
244            objects: Arc::new(ObjectRegistry::default()),
245            runtime: runtime.clone(),
246            startup_info: Arc::new(RwLock::new(info)),
247            #[cfg(target_os = "linux")]
248            glib_loop: Arc::new(RwLock::new(linux_opt)),
249        })
250    }
251
252    #[cfg(target_os = "linux")]
253    pub fn get_platform(&self) -> Result<PlatformType, ObsError> {
254        self.runtime.get_platform()
255    }
256
257    pub fn get_version(&self) -> Result<String, ObsError> {
258        Self::get_version_global()
259    }
260
261    pub fn get_version_global() -> Result<String, ObsError> {
262        unsafe {
263            #[allow(unknown_lints)]
264            #[allow(ensure_obs_call_in_runtime)]
265            // Safety: This is fine, it just returns a globally allocated variable
266            let version = libobs::obs_get_version_string();
267            if version.is_null() {
268                return Err(ObsError::NullPointer(Some(
269                    "OBS version string".to_string(),
270                )));
271            }
272            let version_cstr = CStr::from_ptr(version);
273            Ok(version_cstr.to_string_lossy().into_owned())
274        }
275    }
276
277    pub fn log(&self, level: ObsLogLevel, msg: &str) {
278        let mut log = LOGGER
279            .lock()
280            .unwrap_or_else(|poisoned| poisoned.into_inner());
281        log.log(level, msg.to_string());
282    }
283
284    /// Resets the OBS video context. This is often called
285    /// when one wants to change a setting related to the
286    /// OBS video info sent on startup.
287    ///
288    /// It is important to register your video encoders to
289    /// a video handle after you reset the video context
290    /// if you are using a video handle other than the
291    /// main video handle. For convenience, this function
292    /// sets all video encoder back to the main video handler
293    /// by default.
294    ///
295    /// Note that you cannot reset the graphics module
296    /// without destroying the entire OBS context. Trying
297    /// so will result in an error.
298    pub fn reset_video(&mut self, ovi: ObsVideoInfo) -> Result<(), ObsError> {
299        // You cannot change the graphics module without
300        // completely destroying the entire OBS context.
301        if self
302            .startup_info
303            .read()
304            .map_err(|_| {
305                ObsError::LockError("Failed to acquire read lock on startup info".to_string())
306            })?
307            .obs_video_info
308            .graphics_module()
309            != ovi.graphics_module()
310        {
311            return Err(ObsError::ResetVideoFailureGraphicsModule);
312        }
313
314        if self.objects.any_output_active()? {
315            return Err(ObsError::ResetVideoFailureOutputActive);
316        }
317
318        // Resets the video context. Note that this
319        // is similar to Self::reset_video, but it
320        // does not call that function because the
321        // ObsContext struct is not created yet,
322        // and also because there is no need to free
323        // anything tied to the OBS context.
324        let vid_ptr = Sendable(ovi.as_ptr());
325        let reset_video_status = run_with_obs!(self.runtime, (vid_ptr), move || unsafe {
326            // Safety: OVI is still in scope, so the pointer is valid as well.
327            libobs::obs_reset_video(vid_ptr.0)
328        })?;
329
330        let reset_video_status = num_traits::FromPrimitive::from_i32(reset_video_status);
331
332        let reset_video_status = match reset_video_status {
333            Some(x) => x,
334            None => ObsResetVideoStatus::Failure,
335        };
336
337        if reset_video_status == ObsResetVideoStatus::Success {
338            self.startup_info
339                .write()
340                .map_err(|_| {
341                    ObsError::LockError("Failed to acquire write lock on startup info".to_string())
342                })?
343                .obs_video_info = ovi;
344
345            Ok(())
346        } else {
347            Err(ObsError::ResetVideoFailure(reset_video_status))
348        }
349    }
350
351    /// Returns a pointer to the video output.
352    ///
353    /// # Safety
354    /// This function is unsafe because it returns a raw pointer that must be handled carefully. Only use this pointer if you REALLY know what you are doing.
355    pub unsafe fn get_video_ptr(&self) -> Result<*mut video_output, ObsError> {
356        // Removed safeguards here because ptr are not sendable and this OBS context should never be used across threads
357        run_with_obs!(self.runtime, || unsafe {
358            // Safety: This can be called as long as OBS hasn't shutdown, which it hasn't.
359            Sendable(libobs::obs_get_video())
360        })
361        .map(|ptr| ptr.0)
362    }
363
364    /// Returns a pointer to the audio output.
365    ///
366    /// # Safety
367    /// This function is unsafe because it returns a raw pointer that must be handled carefully. Only use this pointer if you REALLY know what you are doing.
368    pub unsafe fn get_audio_ptr(&self) -> Result<*mut audio_output, ObsError> {
369        // Removed safeguards here because ptr are not sendable and this OBS context should never be used across threads
370        run_with_obs!(self.runtime, || unsafe {
371            // Safety: This can be called as long as OBS hasn't shutdown, which it hasn't.
372            Sendable(libobs::obs_get_audio())
373        })
374        .map(|ptr| ptr.0)
375    }
376
377    pub fn data(&self) -> Result<ObsData, ObsError> {
378        ObsData::new(self.runtime.clone())
379    }
380
381    pub fn replay_buffer(
382        &mut self,
383        info: OutputInfo,
384    ) -> Result<ObsReplayBufferOutputRef, ObsError> {
385        let output = ObsReplayBufferOutputRef::new(info, self.runtime.clone());
386
387        match output {
388            Ok(x) => {
389                let tmp = x.clone();
390                self.objects.add_output(x)?;
391                Ok(tmp)
392            }
393
394            Err(x) => Err(x),
395        }
396    }
397
398    pub fn output(&mut self, info: OutputInfo) -> Result<ObsOutputRef, ObsError> {
399        let output = ObsOutputRef::new(info, self.runtime.clone());
400
401        match output {
402            Ok(x) => {
403                let tmp = x.clone();
404                self.objects.add_output(x)?;
405                Ok(tmp)
406            }
407
408            Err(x) => Err(x),
409        }
410    }
411
412    pub fn obs_filter(&mut self, info: FilterInfo) -> Result<ObsFilterRef, ObsError> {
413        let filter = ObsFilterRef::new(
414            info.id,
415            info.name,
416            info.settings,
417            info.hotkey_data,
418            self.runtime.clone(),
419        );
420
421        match filter {
422            Ok(x) => {
423                let tmp = x.clone();
424                self.objects.add_filter(x)?;
425                Ok(tmp)
426            }
427
428            Err(x) => Err(x),
429        }
430    }
431
432    /// Creates a new display and returns its ID.
433    ///
434    /// You must call `update_color_space` on the display when the window is moved, resized or the display settings change.
435    ///
436    /// Note: When calling `set_size` or `set_pos`, `update_color_space` is called automatically.
437    ///
438    /// Another note: On Linux, this method is unsafe because you must ensure that every display reference is dropped before your window exits.
439    #[cfg(not(target_os = "linux"))]
440    pub fn display(&mut self, data: ObsDisplayCreationData) -> Result<ObsDisplayRef, ObsError> {
441        self.inner_display_fn(data)
442    }
443
444    /// Creates a new display and returns its ID.
445    ///
446    /// You must call `update_color_space` on the display when the window is moved, resized or the display settings change.
447    ///
448    /// # Safety
449    /// All references of the `ObsDisplayRef` **MUST** be dropped before your window closes, otherwise you **will** have crashes.
450    /// This includes calling `remove_display` or `remove_display_by_id` to remove the display from the context.
451    ///
452    /// Also on X11, make sure that the provided window handle was created using the same display as the one provided in the `NixDisplay` in the `StartupInfo`.
453    ///
454    /// Note: When calling `set_size` or `set_pos`, `update_color_space` is called automatically.
455    #[cfg(target_os = "linux")]
456    pub unsafe fn display(
457        &mut self,
458        data: ObsDisplayCreationData,
459    ) -> Result<ObsDisplayRef, ObsError> {
460        self.inner_display_fn(data)
461    }
462
463    /// This function is used internally to create displays.
464    fn inner_display_fn(
465        &mut self,
466        data: ObsDisplayCreationData,
467    ) -> Result<ObsDisplayRef, ObsError> {
468        #[cfg(target_os = "linux")]
469        {
470            // We'll need to check if a custom display was provided because libobs will crash if the display didn't create the window the user is giving us
471            // X11 allows having a separate display however.
472            let nix_display = self
473                .startup_info
474                .read()
475                .map_err(|_| {
476                    ObsError::LockError("Failed to acquire read lock on startup info".to_string())
477                })?
478                .nix_display
479                .clone();
480
481            let is_wayland_handle = data.window_handle.is_wayland;
482            if is_wayland_handle && nix_display.is_none() {
483                return Err(ObsError::DisplayCreationError(
484                    "Wayland window handle provided but no NixDisplay was set in StartupInfo."
485                        .to_string(),
486                ));
487            }
488
489            if let Some(nix_display) = &nix_display {
490                if is_wayland_handle {
491                    match nix_display {
492                        crate::utils::NixDisplay::X11(_display) => {
493                            return Err(ObsError::DisplayCreationError(
494                                "Provided NixDisplay is X11, but the window handle is Wayland."
495                                    .to_string(),
496                            ));
497                        }
498                        crate::utils::NixDisplay::Wayland(display) => {
499                            use crate::utils::linux::wl_proxy_get_display;
500                            if !data.window_handle.is_wayland {
501                                return Err(ObsError::DisplayCreationError(
502                            "Provided window handle is not a Wayland handle, but the NixDisplay is Wayland.".to_string(),
503                        ));
504                            }
505
506                            let surface_handle = data.window_handle.window.0.display;
507                            let display_from_surface = unsafe {
508                                // Safety: The display handle is valid as long as the surface is valid.
509                                wl_proxy_get_display(surface_handle)
510                            };
511                            if let Err(e) = display_from_surface {
512                                log::warn!("Could not get display from surface handle on wayland. Make sure your wayland client is at least version 1.23. Error: {:?}", e);
513                            } else if let Ok(display_from_surface) = display_from_surface {
514                                if display_from_surface != display.as_ptr() {
515                                    return Err(ObsError::DisplayCreationError(
516                            "Provided surface handle's Wayland display does not match the NixDisplay's Wayland display.".to_string(),
517                        ));
518                                }
519                            }
520                        }
521                    }
522                }
523            }
524        }
525
526        let display = ObsDisplayRef::new(data, self.runtime.clone())
527            .map_err(|e| ObsError::DisplayCreationError(e.to_string()))?;
528
529        self.objects.add_display(display.clone())?;
530        Ok(display)
531    }
532
533    pub fn remove_display(&mut self, display: &ObsDisplayRef) -> Result<(), ObsError> {
534        self.remove_display_by_id(display.id())
535    }
536
537    pub fn remove_display_by_id(&mut self, id: usize) -> Result<(), ObsError> {
538        self.objects.remove_display(id)
539    }
540
541    pub fn get_display_by_id(&self, id: usize) -> Result<Option<ObsDisplayRef>, ObsError> {
542        self.objects.display(id)
543    }
544
545    pub fn get_output(&self, name: &str) -> Result<Option<Arc<dyn ObsOutputTrait>>, ObsError> {
546        self.objects.output(name)
547    }
548
549    pub fn update_output(&self, name: &str, settings: ObsData) -> Result<(), ObsError> {
550        self.objects
551            .output(name)?
552            .ok_or(ObsError::OutputNotFound)?
553            .update_settings(settings)
554    }
555
556    pub fn get_filter(&self, name: &str) -> Result<Option<ObsFilterRef>, ObsError> {
557        self.objects.filter(name)
558    }
559
560    /// Creates a new scene
561    ///
562    /// If the channel is provided, the scene will be set to that output channel.
563    ///
564    /// There are 64 channels that you can assign scenes to,
565    /// which will draw on top of each other in ascending index order
566    /// when a output is rendered.
567    ///
568    /// # Arguments
569    /// * `name` - The name of the scene. This must be unique.
570    /// * `channel` - Optional channel to bind the scene to. If provided, the scene will be set as active for that channel.
571    ///
572    /// # Returns
573    /// A Result containing the new ObsSceneRef or an error
574    pub fn scene<T: Into<ObsString> + Send + Sync>(
575        &mut self,
576        name: T,
577        channel: Option<u32>,
578    ) -> Result<ObsSceneRef, ObsError> {
579        let scene = ObsSceneRef::new(name.into(), self.runtime.clone())?;
580
581        let tmp = scene.clone();
582        self.objects.add_scene(scene)?;
583
584        if let Some(channel) = channel {
585            tmp.set_to_channel(channel)?;
586        }
587        Ok(tmp)
588    }
589
590    pub fn get_scene(&self, name: &str) -> Result<Option<ObsSceneRef>, ObsError> {
591        self.objects.scene(name)
592    }
593
594    pub fn source_builder<T: ObsSourceBuilder, K: Into<ObsString> + Send + Sync>(
595        &self,
596        name: K,
597    ) -> Result<T, ObsError> {
598        T::new(name.into(), self.runtime.clone())
599    }
600}