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//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
28//! use libobs_wrapper::context::ObsContext;
29//!
30//! let context = ObsContext::builder().start().await?;
31//! # Ok(())
32//! # }
33//! ```
34//!
35//! For more examples refer to the [examples](https://github.com/joshprk/libobs-rs/tree/main/examples) directory in the repository.
36
37use std::{collections::HashMap, ffi::CStr, pin::Pin, sync::Arc, thread::ThreadId};
38
39use crate::{
40    data::{output::ObsOutputRef, video::ObsVideoInfo, ObsData},
41    display::{ObsDisplayCreationData, ObsDisplayRef},
42    enums::{ObsLogLevel, ObsResetVideoStatus},
43    logger::LOGGER,
44    run_with_obs,
45    runtime::{ObsRuntime, ObsRuntimeReturn},
46    scenes::ObsSceneRef,
47    sources::ObsSourceBuilder,
48    unsafe_send::Sendable,
49    utils::{
50        ObsError, ObsModules, ObsString, OutputInfo, StartupInfo,
51    },
52};
53use crate::utils::async_sync::{Mutex, RwLock};
54use getters0::Getters;
55use libobs::{audio_output, obs_scene_t, video_output};
56
57lazy_static::lazy_static! {
58    pub(crate) static ref OBS_THREAD_ID: Mutex<Option<ThreadId>> = Mutex::new(None);
59}
60
61// Note to developers of this library:
62// I've updated everything in the ObsContext to use Rc and RefCell.
63// Then the obs context shutdown hook is given to each children of for example scenes and displays.
64// That way, obs is not shut down as long as there are still displays or scenes alive.
65// This is a bit of a hack, but it works would be glad to hear your thoughts on this.
66
67/// Interface to the OBS context. Only one context
68/// can exist across all threads and any attempt to
69/// create a new context while there is an existing
70/// one will error.
71///
72/// Note that the order of the struct values is
73/// important! OBS is super specific about how it
74/// does everything. Things are freed early to
75/// latest from top to bottom.
76#[derive(Debug, Getters, Clone)]
77#[skip_new]
78pub struct ObsContext {
79    /// Stores startup info for safe-keeping. This
80    /// prevents any use-after-free as these do not
81    /// get copied in libobs.
82    startup_info: Arc<RwLock<StartupInfo>>,
83
84    #[get_mut]
85    // Key is display id, value is the display fixed in heap
86    displays: Arc<RwLock<HashMap<usize, Arc<Pin<Box<ObsDisplayRef>>>>>>,
87
88    /// Outputs must be stored in order to prevent
89    /// early freeing.
90    #[allow(dead_code)]
91    #[get_mut]
92    pub(crate) outputs: Arc<RwLock<Vec<ObsOutputRef>>>,
93
94    #[get_mut]
95    pub(crate) scenes: Arc<RwLock<Vec<ObsSceneRef>>>,
96
97    #[skip_getter]
98    pub(crate) active_scene: Arc<RwLock<Option<Sendable<*mut obs_scene_t>>>>,
99
100    #[skip_getter]
101    pub(crate) _obs_modules: Arc<ObsModules>,
102
103    /// This struct must be the last element which makes sure
104    /// that everything else has been freed already before the runtime
105    /// shuts down
106    pub(crate) runtime: ObsRuntime,
107}
108
109#[cfg(not(feature = "bootstrapper"))]
110pub type ObsContextReturn = ObsContext;
111#[cfg(feature = "bootstrapper")]
112pub enum ObsContextReturn {
113    /// The OBS context is ready to use
114    Done(ObsContext),
115
116    /// The application must be restarted to apply OBS updates
117    Restart,
118}
119
120impl ObsContext {
121    pub fn builder() -> StartupInfo {
122        StartupInfo::new()
123    }
124
125    /// Initializes libobs on the current thread.
126    ///
127    /// Note that there can be only one ObsContext
128    /// initialized at a time. This is because
129    /// libobs is not completely thread-safe.
130    ///
131    /// Also note that this might leak a very tiny
132    /// amount of memory. As a result, it is
133    /// probably a good idea not to restart the
134    /// OBS context repeatedly over a very long
135    /// period of time. Unfortunately the memory
136    /// leak is caused by a bug in libobs itself.
137    ///
138    /// If the `bootstrapper` feature is enabled, and ObsContextReturn::Restart is returned,
139    /// the application must be restarted to apply the updates and initialization can not continue.
140    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
141    pub async fn new(info: StartupInfo) -> Result<ObsContextReturn, ObsError> {
142        // Spawning runtime, I'll keep this as function for now
143        let runtime = ObsRuntime::startup(info).await?;
144
145        if matches!(runtime, ObsRuntimeReturn::Restart) {
146            return Ok(ObsContextReturn::Restart);
147        }
148
149        let (runtime, obs_modules, info) = match runtime {
150            ObsRuntimeReturn::Done(r) => r,
151            ObsRuntimeReturn::Restart => unreachable!(),
152        };
153
154        let context = Self {
155            _obs_modules: Arc::new(obs_modules),
156            active_scene: Default::default(),
157            displays: Default::default(),
158            outputs: Default::default(),
159            scenes: Default::default(),
160            runtime,
161            startup_info: Arc::new(RwLock::new(info)),
162        };
163
164        #[cfg(feature = "bootstrapper")]
165        return Ok(ObsContextReturn::Done(context));
166
167        #[cfg(not(feature = "bootstrapper"))]
168        return Ok(context);
169    }
170
171    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
172    pub async fn get_version(&self) -> Result<String, ObsError> {
173        let res = run_with_obs!(self.runtime, || unsafe {
174            let version = libobs::obs_get_version_string();
175            let version_cstr = CStr::from_ptr(version);
176
177            version_cstr.to_string_lossy().into_owned()
178        }).await?;
179
180        Ok(res)
181    }
182
183    pub fn log(&self, level: ObsLogLevel, msg: &str) {
184        let mut log = LOGGER.lock().unwrap();
185        log.log(level, msg.to_string());
186    }
187
188    /// Resets the OBS video context. This is often called
189    /// when one wants to change a setting related to the
190    /// OBS video info sent on startup.
191    ///
192    /// It is important to register your video encoders to
193    /// a video handle after you reset the video context
194    /// if you are using a video handle other than the
195    /// main video handle. For convenience, this function
196    /// sets all video encoder back to the main video handler
197    /// by default.
198    ///
199    /// Note that you cannot reset the graphics module
200    /// without destroying the entire OBS context. Trying
201    /// so will result in an error.
202    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
203    pub async fn reset_video(&mut self, ovi: ObsVideoInfo) -> Result<(), ObsError> {
204        // You cannot change the graphics module without
205        // completely destroying the entire OBS context.
206        if self
207            .startup_info
208            .read()
209            .await
210            .obs_video_info
211            .graphics_module()
212            != ovi.graphics_module()
213        {
214            return Err(ObsError::ResetVideoFailureGraphicsModule);
215        }
216
217        // Resets the video context. Note that this
218        // is similar to Self::reset_video, but it
219        // does not call that function because the
220        // ObsContext struct is not created yet,
221        // and also because there is no need to free
222        // anything tied to the OBS context.
223        let mut vid = self.startup_info.write().await;
224        let vid_ptr = Sendable(vid.obs_video_info.as_ptr());
225
226        let reset_video_status = run_with_obs!(self.runtime, (vid_ptr), move || unsafe {
227            libobs::obs_reset_video(vid_ptr)
228        }).await?;
229
230        drop(vid);
231        let reset_video_status = num_traits::FromPrimitive::from_i32(reset_video_status);
232
233        let reset_video_status = match reset_video_status {
234            Some(x) => x,
235            None => ObsResetVideoStatus::Failure,
236        };
237
238        if reset_video_status != ObsResetVideoStatus::Success {
239            return Err(ObsError::ResetVideoFailure(reset_video_status));
240        } else {
241            let outputs = self.outputs.read().await.clone();
242            let mut video_encoders = vec![];
243
244            for output in outputs.iter() {
245                let encoders = output.get_video_encoders().await;
246                video_encoders.extend(encoders.into_iter().map(|e| e.as_ptr()));
247            }
248
249            let vid_ptr = self.get_video_ptr().await?;
250            run_with_obs!(self.runtime, (vid_ptr), move || unsafe {
251                for encoder_ptr in video_encoders.into_iter() {
252                    libobs::obs_encoder_set_video(encoder_ptr.0, vid_ptr);
253                }
254            }).await?;
255
256            self.startup_info.write().await.obs_video_info = ovi;
257            return Ok(());
258        }
259    }
260
261    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
262    pub async fn get_video_ptr(&self) -> Result<Sendable<*mut video_output>, ObsError> {
263        // Removed safeguards here because ptr are not sendable and this OBS context should never be used across threads
264        run_with_obs!(self.runtime, || unsafe {
265            Sendable(libobs::obs_get_video())
266        }).await
267    }
268
269    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
270    pub async fn get_audio_ptr(&self) -> Result<Sendable<*mut audio_output>, ObsError> {
271        // Removed safeguards here because ptr are not sendable and this OBS context should never be used across threads
272        run_with_obs!(self.runtime, || unsafe {
273            Sendable(libobs::obs_get_audio())
274        }).await
275    }
276
277    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
278    pub async fn data(&self) -> Result<ObsData, ObsError> {
279        ObsData::new(self.runtime.clone()).await
280    }
281
282    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
283    pub async fn output(&mut self, info: OutputInfo) -> Result<ObsOutputRef, ObsError> {
284        let output = ObsOutputRef::new(info, self.runtime.clone()).await;
285
286        return match output {
287            Ok(x) => {
288                let tmp = x.clone();
289                self.outputs.write().await.push(x);
290                Ok(tmp)
291            }
292
293            Err(x) => Err(x),
294        };
295    }
296
297    /// Creates a new display and returns its ID.
298    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
299    pub async fn display(
300        &mut self,
301        data: ObsDisplayCreationData,
302    ) -> Result<Pin<Box<ObsDisplayRef>>, ObsError> {
303        let display = ObsDisplayRef::new(data, self.runtime.clone())
304            .await
305            .map_err(|e| ObsError::DisplayCreationError(e.to_string()))?;
306
307        let display_clone = display.clone();
308
309        let id = display.id();
310        self.displays.write().await.insert(id, Arc::new(display));
311        Ok(display_clone)
312    }
313
314    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
315    pub async fn remove_display(&mut self, display: &ObsDisplayRef) {
316        self.remove_display_by_id(display.id()).await;
317    }
318
319    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
320    pub async fn remove_display_by_id(&mut self, id: usize) {
321        self.displays.write().await.remove(&id);
322    }
323
324    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
325    pub async fn get_display_by_id(&self, id: usize) -> Option<Arc<Pin<Box<ObsDisplayRef>>>> {
326        self.displays.read().await.get(&id).cloned()
327    }
328
329    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
330    pub async fn get_output(&mut self, name: &str) -> Option<ObsOutputRef> {
331        self.outputs
332            .read()
333            .await
334            .iter()
335            .find(|x| x.name().to_string().as_str() == name)
336            .map(|e| e.clone())
337    }
338
339    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
340    pub async fn update_output(&mut self, name: &str, settings: ObsData) -> Result<(), ObsError> {
341        match self
342            .outputs
343            .write()
344            .await
345            .iter_mut()
346            .find(|x| x.name().to_string().as_str() == name)
347        {
348            Some(output) => output.update_settings(settings).await,
349            None => Err(ObsError::OutputNotFound),
350        }
351    }
352
353    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
354    pub async fn scene<T: Into<ObsString> + Send + Sync>(
355        &mut self,
356        name: T,
357    ) -> Result<ObsSceneRef, ObsError> {
358        let scene =
359            ObsSceneRef::new(name.into(), self.active_scene.clone(), self.runtime.clone()).await?;
360
361        let tmp = scene.clone();
362        self.scenes.write().await.push(scene);
363
364        Ok(tmp)
365    }
366
367    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
368    pub async fn get_scene(&mut self, name: &str) -> Option<ObsSceneRef> {
369        self.scenes
370            .read()
371            .await
372            .iter()
373            .find(|x| x.name().to_string().as_str() == name)
374            .map(|e| e.clone())
375    }
376
377    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
378    pub async fn source_builder<T: ObsSourceBuilder, K: Into<ObsString> + Send + Sync>(
379        &self,
380        name: K,
381    ) -> Result<T, ObsError> {
382        T::new(name.into(), self.runtime.clone()).await
383    }
384}