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//! - Download and bootstrap OBS binaries at runtime
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
39use std::{
40    collections::HashMap,
41    ffi::CStr,
42    sync::{Arc, Mutex, RwLock},
43    thread::ThreadId,
44};
45
46use crate::display::{ObsDisplayCreationData, ObsDisplayRef};
47use crate::{
48    data::{output::ObsOutputRef, video::ObsVideoInfo, ObsData},
49    enums::{ObsLogLevel, ObsResetVideoStatus},
50    logger::LOGGER,
51    run_with_obs,
52    runtime::ObsRuntime,
53    scenes::ObsSceneRef,
54    sources::{ObsFilterRef, ObsSourceBuilder},
55    unsafe_send::Sendable,
56    utils::{FilterInfo, ObsError, ObsModules, ObsString, OutputInfo, StartupInfo},
57};
58use getters0::Getters;
59use libobs::{audio_output, video_output};
60
61lazy_static::lazy_static! {
62    pub(crate) static ref OBS_THREAD_ID: Mutex<Option<ThreadId>> = Mutex::new(None);
63}
64
65/// Interface to the OBS context. Only one context
66/// can exist across all threads and any attempt to
67/// create a new context while there is an existing
68/// one will error.
69///
70/// Note that the order of the struct values is
71/// important! OBS is super specific about how it
72/// does everything. Things are freed early to
73/// latest from top to bottom.
74#[derive(Debug, Getters, Clone)]
75#[skip_new]
76pub struct ObsContext {
77    /// Stores startup info for safe-keeping. This
78    /// prevents any use-after-free as these do not
79    /// get copied in libobs.
80    startup_info: Arc<RwLock<StartupInfo>>,
81    #[get_mut]
82    // Key is display id, value is the display fixed in heap
83    displays: Arc<RwLock<HashMap<usize, ObsDisplayRef>>>,
84
85    /// Outputs must be stored in order to prevent
86    /// early freeing.
87    #[allow(dead_code)]
88    #[get_mut]
89    pub(crate) outputs: Arc<RwLock<Vec<ObsOutputRef>>>,
90
91    #[get_mut]
92    pub(crate) scenes: Arc<RwLock<Vec<ObsSceneRef>>>,
93
94    // Filters are on the level of the context because they are not scene-specific
95    #[get_mut]
96    pub(crate) filters: Arc<RwLock<Vec<ObsFilterRef>>>,
97
98    #[skip_getter]
99    /// Contains active scenes mapped by their channel they are bound to
100    pub(crate) active_scenes: Arc<RwLock<HashMap<u32, ObsSceneRef>>>,
101
102    #[skip_getter]
103    pub(crate) _obs_modules: Arc<ObsModules>,
104
105    /// This struct must be the last element which makes sure
106    /// that everything else has been freed already before the runtime
107    /// shuts down
108    pub(crate) runtime: ObsRuntime,
109
110    #[cfg(target_os = "linux")]
111    pub(crate) glib_loop: Arc<RwLock<Option<crate::utils::linux::LinuxGlibLoop>>>,
112}
113
114impl ObsContext {
115    /// Checks if the installed OBS version matches the expected version.
116    /// Returns true if the major version matches, false otherwise.
117    pub fn check_version_compatibility() -> bool {
118        unsafe {
119            let version = libobs::obs_get_version_string();
120            if version.is_null() {
121                return false;
122            }
123
124            let version_str = match CStr::from_ptr(version).to_str() {
125                Ok(s) => s,
126                Err(_) => return false,
127            };
128
129            let version_parts: Vec<&str> = version_str.split('.').collect();
130            if version_parts.len() != 3 {
131                return false;
132            }
133
134            let major = match version_parts[0].parse::<u64>() {
135                Ok(v) => v,
136                Err(_) => return false,
137            };
138
139            major == libobs::LIBOBS_API_MAJOR_VER as u64
140        }
141    }
142
143    pub fn builder() -> StartupInfo {
144        StartupInfo::new()
145    }
146
147    /// Initializes libobs on the current thread.
148    ///
149    /// Note that there can be only one ObsContext
150    /// initialized at a time. This is because
151    /// libobs is not completely thread-safe.
152    ///
153    /// Also note that this might leak a very tiny
154    /// amount of memory. As a result, it is
155    /// probably a good idea not to restart the
156    /// OBS context repeatedly over a very long
157    /// period of time. Unfortunately the memory
158    /// leak is caused by a bug in libobs itself.
159    ///
160    /// On Linux, make sure to call `ObsContext::check_version_compatibility` before
161    /// initializing the context. If that method returns false, it may be possible for the binary to crash.
162    ///
163    /// If initialization fails, an `ObsError` is returned.
164    pub fn new(info: StartupInfo) -> Result<ObsContext, ObsError> {
165        // Spawning runtime, I'll keep this as function for now
166        let (runtime, obs_modules, info) = ObsRuntime::startup(info)?;
167        #[cfg(target_os = "linux")]
168        let linux_opt = if info.start_glib_loop {
169            Some(crate::utils::linux::LinuxGlibLoop::new())
170        } else {
171            None
172        };
173
174        let active_scenes: Arc<RwLock<HashMap<u32, ObsSceneRef>>> = Default::default();
175        Ok(Self {
176            _obs_modules: Arc::new(obs_modules),
177            active_scenes: active_scenes.clone(),
178            displays: Default::default(),
179            outputs: Default::default(),
180            scenes: Default::default(),
181            filters: Default::default(),
182            runtime: runtime.clone(),
183            startup_info: Arc::new(RwLock::new(info)),
184            #[cfg(target_os = "linux")]
185            glib_loop: Arc::new(RwLock::new(linux_opt)),
186        })
187    }
188
189    pub fn get_version(&self) -> Result<String, ObsError> {
190        Self::get_version_global()
191    }
192
193    pub fn get_version_global() -> Result<String, ObsError> {
194        unsafe {
195            let version = libobs::obs_get_version_string();
196            let version_cstr = CStr::from_ptr(version);
197
198            let version = version_cstr.to_string_lossy().into_owned();
199
200            Ok(version)
201        }
202    }
203
204    pub fn log(&self, level: ObsLogLevel, msg: &str) {
205        let mut log = LOGGER.lock().unwrap();
206        log.log(level, msg.to_string());
207    }
208
209    /// Resets the OBS video context. This is often called
210    /// when one wants to change a setting related to the
211    /// OBS video info sent on startup.
212    ///
213    /// It is important to register your video encoders to
214    /// a video handle after you reset the video context
215    /// if you are using a video handle other than the
216    /// main video handle. For convenience, this function
217    /// sets all video encoder back to the main video handler
218    /// by default.
219    ///
220    /// Note that you cannot reset the graphics module
221    /// without destroying the entire OBS context. Trying
222    /// so will result in an error.
223    pub fn reset_video(&mut self, ovi: ObsVideoInfo) -> Result<(), ObsError> {
224        // You cannot change the graphics module without
225        // completely destroying the entire OBS context.
226        if self
227            .startup_info
228            .read()
229            .map_err(|_| {
230                ObsError::LockError("Failed to acquire read lock on startup info".to_string())
231            })?
232            .obs_video_info
233            .graphics_module()
234            != ovi.graphics_module()
235        {
236            return Err(ObsError::ResetVideoFailureGraphicsModule);
237        }
238
239        let has_active_outputs = {
240            self.outputs
241                .read()
242                .map_err(|_| {
243                    ObsError::LockError("Failed to acquire read lock on outputs".to_string())
244                })?
245                .iter()
246                .any(|output| output.is_active().unwrap_or_default())
247        };
248
249        if has_active_outputs {
250            return Err(ObsError::ResetVideoFailureOutputActive);
251        }
252
253        // Resets the video context. Note that this
254        // is similar to Self::reset_video, but it
255        // does not call that function because the
256        // ObsContext struct is not created yet,
257        // and also because there is no need to free
258        // anything tied to the OBS context.
259        let vid_ptr = Sendable(ovi.as_ptr());
260        let reset_video_status = run_with_obs!(self.runtime, (vid_ptr), move || unsafe {
261            libobs::obs_reset_video(vid_ptr)
262        })?;
263
264        let reset_video_status = num_traits::FromPrimitive::from_i32(reset_video_status);
265
266        let reset_video_status = match reset_video_status {
267            Some(x) => x,
268            None => ObsResetVideoStatus::Failure,
269        };
270
271        if reset_video_status == ObsResetVideoStatus::Success {
272            self.startup_info
273                .write()
274                .map_err(|_| {
275                    ObsError::LockError("Failed to acquire write lock on startup info".to_string())
276                })?
277                .obs_video_info = ovi;
278
279            Ok(())
280        } else {
281            Err(ObsError::ResetVideoFailure(reset_video_status))
282        }
283    }
284
285    /// Returns a pointer to the video output.
286    ///
287    /// # Safety
288    /// 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.
289    pub unsafe fn get_video_ptr(&self) -> Result<Sendable<*mut video_output>, ObsError> {
290        // Removed safeguards here because ptr are not sendable and this OBS context should never be used across threads
291        run_with_obs!(self.runtime, || unsafe {
292            Sendable(libobs::obs_get_video())
293        })
294    }
295
296    /// Returns a pointer to the audio output.
297    ///
298    /// # Safety
299    /// 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.
300    pub unsafe fn get_audio_ptr(&self) -> Result<Sendable<*mut audio_output>, ObsError> {
301        // Removed safeguards here because ptr are not sendable and this OBS context should never be used across threads
302        run_with_obs!(self.runtime, || unsafe {
303            Sendable(libobs::obs_get_audio())
304        })
305    }
306
307    pub fn data(&self) -> Result<ObsData, ObsError> {
308        ObsData::new(self.runtime.clone())
309    }
310
311    pub fn output(&mut self, info: OutputInfo) -> Result<ObsOutputRef, ObsError> {
312        let output = ObsOutputRef::new(info, self.runtime.clone());
313
314        match output {
315            Ok(x) => {
316                let tmp = x.clone();
317                self.outputs
318                    .write()
319                    .map_err(|_| {
320                        ObsError::LockError("Failed to acquire write lock on outputs".to_string())
321                    })?
322                    .push(x);
323                Ok(tmp)
324            }
325
326            Err(x) => Err(x),
327        }
328    }
329
330    pub fn obs_filter(&mut self, info: FilterInfo) -> Result<ObsFilterRef, ObsError> {
331        let filter = ObsFilterRef::new(
332            info.id,
333            info.name,
334            info.settings,
335            info.hotkey_data,
336            self.runtime.clone(),
337        );
338
339        match filter {
340            Ok(x) => {
341                let tmp = x.clone();
342                self.filters
343                    .write()
344                    .map_err(|_| {
345                        ObsError::LockError("Failed to acquire write lock on filters".to_string())
346                    })?
347                    .push(x);
348                Ok(tmp)
349            }
350
351            Err(x) => Err(x),
352        }
353    }
354
355    /// Creates a new display and returns its ID.
356    ///
357    /// You must call `update_color_space` on the display when the window is moved, resized or the display settings change.
358    ///
359    /// Note: When calling `set_size` or `set_pos`, `update_color_space` is called automatically.
360    ///
361    /// Another note: On Linux, this method is unsafe because you must ensure that every display reference is dropped before your window exits.
362    #[cfg(not(target_os = "linux"))]
363    pub fn display(&mut self, data: ObsDisplayCreationData) -> Result<ObsDisplayRef, ObsError> {
364        self.inner_display_fn(data)
365    }
366
367    /// Creates a new display and returns its ID.
368    ///
369    /// You must call `update_color_space` on the display when the window is moved, resized or the display settings change.
370    ///
371    /// # Safety
372    /// All references of the `ObsDisplayRef` **MUST** be dropped before your window closes, otherwise you **will** have crashes.
373    /// This includes calling `remove_display` or `remove_display_by_id` to remove the display from the context.
374    ///
375    /// 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`.
376    ///
377    /// Note: When calling `set_size` or `set_pos`, `update_color_space` is called automatically.
378    #[cfg(target_os = "linux")]
379    pub unsafe fn display(
380        &mut self,
381        data: ObsDisplayCreationData,
382    ) -> Result<ObsDisplayRef, ObsError> {
383        self.inner_display_fn(data)
384    }
385
386    /// This function is used internally to create displays.
387    fn inner_display_fn(
388        &mut self,
389        data: ObsDisplayCreationData,
390    ) -> Result<ObsDisplayRef, ObsError> {
391        #[cfg(target_os = "linux")]
392        {
393            // 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
394            // X11 allows having a separate display however.
395            let nix_display = self
396                .startup_info
397                .read()
398                .map_err(|_| {
399                    ObsError::LockError("Failed to acquire read lock on startup info".to_string())
400                })?
401                .nix_display
402                .clone();
403
404            let is_wayland_handle = data.window_handle.is_wayland;
405            if is_wayland_handle && nix_display.is_none() {
406                return Err(ObsError::DisplayCreationError(
407                    "Wayland window handle provided but no NixDisplay was set in StartupInfo."
408                        .to_string(),
409                ));
410            }
411
412            if let Some(nix_display) = &nix_display {
413                if is_wayland_handle {
414                    match nix_display {
415                        crate::utils::NixDisplay::X11(_display) => {
416                            return Err(ObsError::DisplayCreationError(
417                                "Provided NixDisplay is X11, but the window handle is Wayland."
418                                    .to_string(),
419                            ));
420                        }
421                        crate::utils::NixDisplay::Wayland(display) => {
422                            use crate::utils::linux::wl_proxy_get_display;
423                            if !data.window_handle.is_wayland {
424                                return Err(ObsError::DisplayCreationError(
425                            "Provided window handle is not a Wayland handle, but the NixDisplay is Wayland.".to_string(),
426                        ));
427                            }
428
429                            let surface_handle = data.window_handle.window.0.display;
430                            let display_from_surface = wl_proxy_get_display(surface_handle);
431                            if let Err(e) = display_from_surface {
432                                log::warn!("Could not get display from surface handle on wayland. Make sure your wayland client is at least version 1.23. Error: {:?}", e);
433                            } else {
434                                let display_from_surface = display_from_surface.unwrap();
435                                if display_from_surface != display.0 {
436                                    return Err(ObsError::DisplayCreationError(
437                            "Provided surface handle's Wayland display does not match the NixDisplay's Wayland display.".to_string(),
438                        ));
439                                }
440                            }
441                        }
442                    }
443                }
444            }
445        }
446
447        let display = ObsDisplayRef::new(data, self.runtime.clone())
448            .map_err(|e| ObsError::DisplayCreationError(e.to_string()))?;
449
450        let id = display.id();
451        self.displays
452            .write()
453            .map_err(|_| {
454                ObsError::LockError("Failed to acquire write lock on displays".to_string())
455            })?
456            .insert(id, display.clone());
457
458        Ok(display)
459    }
460
461    pub fn remove_display(&mut self, display: &ObsDisplayRef) -> Result<(), ObsError> {
462        self.remove_display_by_id(display.id())
463    }
464
465    pub fn remove_display_by_id(&mut self, id: usize) -> Result<(), ObsError> {
466        self.displays
467            .write()
468            .map_err(|_| {
469                ObsError::LockError("Failed to acquire write lock on displays".to_string())
470            })?
471            .remove(&id);
472
473        Ok(())
474    }
475
476    pub fn get_display_by_id(&self, id: usize) -> Result<Option<ObsDisplayRef>, ObsError> {
477        let d = self
478            .displays
479            .read()
480            .map_err(|_| {
481                ObsError::LockError("Failed to acquire read lock on displays".to_string())
482            })?
483            .get(&id)
484            .cloned();
485
486        Ok(d)
487    }
488
489    pub fn get_output(&mut self, name: &str) -> Result<Option<ObsOutputRef>, ObsError> {
490        let o = self
491            .outputs
492            .read()
493            .map_err(|_| ObsError::LockError("Failed to acquire read lock on outputs".to_string()))?
494            .iter()
495            .find(|x| x.name().to_string().as_str() == name)
496            .cloned();
497
498        Ok(o)
499    }
500
501    pub fn update_output(&mut self, name: &str, settings: ObsData) -> Result<(), ObsError> {
502        match self
503            .outputs
504            .write()
505            .map_err(|_| {
506                ObsError::LockError("Failed to acquire write lock on outputs".to_string())
507            })?
508            .iter_mut()
509            .find(|x| x.name().to_string().as_str() == name)
510        {
511            Some(output) => output.update_settings(settings),
512            None => Err(ObsError::OutputNotFound),
513        }
514    }
515
516    pub fn get_filter(&mut self, name: &str) -> Result<Option<ObsFilterRef>, ObsError> {
517        let f = self
518            .filters
519            .read()
520            .map_err(|_| ObsError::LockError("Failed to acquire read lock on filters".to_string()))?
521            .iter()
522            .find(|x| x.name().to_string().as_str() == name)
523            .cloned();
524
525        Ok(f)
526    }
527
528    pub fn scene<T: Into<ObsString> + Send + Sync>(
529        &mut self,
530        name: T,
531    ) -> Result<ObsSceneRef, ObsError> {
532        let scene = ObsSceneRef::new(
533            name.into(),
534            self.active_scenes.clone(),
535            self.runtime.clone(),
536        )?;
537
538        let tmp = scene.clone();
539        self.scenes
540            .write()
541            .map_err(|_| ObsError::LockError("Failed to acquire write lock on scenes".to_string()))?
542            .push(scene);
543
544        Ok(tmp)
545    }
546
547    pub fn get_scene(&mut self, name: &str) -> Result<Option<ObsSceneRef>, ObsError> {
548        let r = self
549            .scenes
550            .read()
551            .map_err(|_| ObsError::LockError("Failed to acquire read lock on scenes".to_string()))?
552            .iter()
553            .find(|x| x.name().to_string().as_str() == name)
554            .cloned();
555        Ok(r)
556    }
557
558    pub fn source_builder<T: ObsSourceBuilder, K: Into<ObsString> + Send + Sync>(
559        &self,
560        name: K,
561    ) -> Result<T, ObsError> {
562        T::new(name.into(), self.runtime.clone())
563    }
564}