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