libobs-wrapper 9.0.4+32.0.2

A safe wrapper around libobs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
//! OBS Context Management
//!
//! This module provides the core functionality for interacting with libobs.
//! The primary type is [`ObsContext`], which serves as the main entry point for
//! all OBS operations.
//!
//! # Overview
//!
//! The `ObsContext` represents an initialized OBS environment and provides methods to:
//! - Initialize the OBS runtime
//! - Create and manage scenes
//! - Create and manage outputs (recording, streaming)
//! - Access and configure video/audio settings
//! - Download and bootstrap OBS binaries at runtime
//!
//! # Thread Safety
//!
//! OBS operations must be performed on a single thread. The `ObsContext` handles
//! this requirement by creating a dedicated thread for OBS operations and providing
//! a thread-safe interface to interact with it.
//!
//! # Examples
//!
//! Creating a basic OBS context:
//!
//! ```no_run
//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
//! use libobs_wrapper::context::ObsContext;
//! use libobs_wrapper::utils::StartupInfo;
//!
//! let info = StartupInfo::default();
//! let context = ObsContext::new(info)?;
//! # Ok(())
//! # }
//! ```
//!
//! For more examples refer to the [examples](https://github.com/libobs-rs/libobs-rs/tree/main/examples) directory in the repository.

use std::{
    collections::HashMap,
    ffi::CStr,
    sync::{Arc, Mutex, RwLock},
    thread::ThreadId,
};

#[cfg(target_os = "linux")]
use crate::utils::initialization::PlatformType;
use crate::{
    data::{
        object::ObsObjectTrait,
        output::{ObsOutputTrait, ObsOutputTraitSealed, ObsReplayBufferOutputRef},
    },
    display::{ObsDisplayCreationData, ObsDisplayRef},
};
use crate::{
    data::{output::ObsOutputRef, video::ObsVideoInfo, ObsData},
    enums::{ObsLogLevel, ObsResetVideoStatus},
    logger::LOGGER,
    run_with_obs,
    runtime::ObsRuntime,
    scenes::ObsSceneRef,
    sources::{ObsFilterRef, ObsSourceBuilder},
    unsafe_send::Sendable,
    utils::{FilterInfo, ObsError, ObsModules, ObsString, OutputInfo, StartupInfo},
};
use getters0::Getters;
use libobs::{audio_output, video_output};

lazy_static::lazy_static! {
    pub(crate) static ref OBS_THREAD_ID: Mutex<Option<ThreadId>> = Mutex::new(None);
}

pub(crate) type GeneralStorage<T> = Arc<RwLock<Vec<Arc<Box<T>>>>>;

/// Interface to the OBS context. Only one context
/// can exist across all threads and any attempt to
/// create a new context while there is an existing
/// one will error.
///
/// Note that the order of the struct values is
/// important! OBS is super specific about how it
/// does everything. Things are freed early to
/// latest from top to bottom.
#[derive(Debug, Getters, Clone)]
#[skip_new]
pub struct ObsContext {
    /// Stores startup info for safe-keeping. This
    /// prevents any use-after-free as these do not
    /// get copied in libobs.
    startup_info: Arc<RwLock<StartupInfo>>,
    #[get_mut]
    // Key is display id, value is the display fixed in heap
    displays: Arc<RwLock<HashMap<usize, ObsDisplayRef>>>,

    /// Outputs must be stored in order to prevent
    /// early freeing.
    #[allow(dead_code)]
    #[get_mut]
    outputs: GeneralStorage<dyn ObsOutputTrait>,

    #[get_mut]
    scenes: Arc<RwLock<Vec<ObsSceneRef>>>,

    // Filters are on the level of the context because they are not scene-specific
    #[get_mut]
    filters: Arc<RwLock<Vec<ObsFilterRef>>>,

    #[skip_getter]
    _obs_modules: Arc<ObsModules>,

    /// This struct must be the last element which makes sure
    /// that everything else has been freed already before the runtime
    /// shuts down
    runtime: ObsRuntime,

    #[cfg(target_os = "linux")]
    glib_loop: Arc<RwLock<Option<crate::utils::linux::LinuxGlibLoop>>>,
}

impl ObsContext {
    /// Checks if the installed OBS version matches the expected version.
    /// Returns true if the major version matches, false otherwise.
    pub fn check_version_compatibility() -> bool {
        // Safety: This is fine, we are just getting a version string, which doesn't allocate any memory or have side effects.
        unsafe {
            #[allow(unknown_lints)]
            #[allow(ensure_obs_call_in_runtime)]
            let version = libobs::obs_get_version_string();
            if version.is_null() {
                return false;
            }

            let version_str = match CStr::from_ptr(version).to_str() {
                Ok(s) => s,
                Err(_) => return false,
            };

            let version_parts: Vec<&str> = version_str.split('.').collect();
            if version_parts.len() != 3 {
                return false;
            }

            let major = match version_parts[0].parse::<u64>() {
                Ok(v) => v,
                Err(_) => return false,
            };

            major == libobs::LIBOBS_API_MAJOR_VER as u64
        }
    }

    pub fn builder() -> StartupInfo {
        StartupInfo::new()
    }

    /// Initializes libobs on the current thread.
    ///
    /// Note that there can be only one ObsContext
    /// initialized at a time. This is because
    /// libobs is not completely thread-safe.
    ///
    /// Also note that this might leak a very tiny
    /// amount of memory. As a result, it is
    /// probably a good idea not to restart the
    /// OBS context repeatedly over a very long
    /// period of time. Unfortunately the memory
    /// leak is caused by a bug in libobs itself.
    ///
    /// On Linux, make sure to call `ObsContext::check_version_compatibility` before
    /// initializing the context. If that method returns false, it may be possible for the binary to crash.
    ///
    /// If initialization fails, an `ObsError` is returned.
    pub fn new(info: StartupInfo) -> Result<ObsContext, ObsError> {
        log::trace!("Getting version number...");

        #[allow(unknown_lints)]
        #[allow(ensure_obs_call_in_runtime)]
        // Safety: This is fine, we are just getting a version number, which does not require
        // to be on the OBS thread.
        let version_numb = unsafe { libobs::obs_get_version() };
        if version_numb == 0 {
            return Err(ObsError::InvalidDll);
        }

        // Spawning runtime, I'll keep this as function for now
        let (runtime, obs_modules, info) = ObsRuntime::startup(info)?;
        #[cfg(target_os = "linux")]
        let linux_opt = if info.start_glib_loop {
            Some(crate::utils::linux::LinuxGlibLoop::new())
        } else {
            None
        };

        Ok(Self {
            _obs_modules: Arc::new(obs_modules),
            displays: Default::default(),
            outputs: Default::default(),
            scenes: Default::default(),
            filters: Default::default(),
            runtime: runtime.clone(),
            startup_info: Arc::new(RwLock::new(info)),
            #[cfg(target_os = "linux")]
            glib_loop: Arc::new(RwLock::new(linux_opt)),
        })
    }

    #[cfg(target_os = "linux")]
    pub fn get_platform(&self) -> Result<PlatformType, ObsError> {
        self.runtime.get_platform()
    }

    pub fn get_version(&self) -> Result<String, ObsError> {
        Self::get_version_global()
    }

    pub fn get_version_global() -> Result<String, ObsError> {
        unsafe {
            #[allow(unknown_lints)]
            #[allow(ensure_obs_call_in_runtime)]
            // Safety: This is fine, it just returns a globally allocated variable
            let version = libobs::obs_get_version_string();
            let version_cstr = CStr::from_ptr(version);

            let version = version_cstr.to_string_lossy().into_owned();

            Ok(version)
        }
    }

    pub fn log(&self, level: ObsLogLevel, msg: &str) {
        let mut log = LOGGER.lock().unwrap();
        log.log(level, msg.to_string());
    }

    /// Resets the OBS video context. This is often called
    /// when one wants to change a setting related to the
    /// OBS video info sent on startup.
    ///
    /// It is important to register your video encoders to
    /// a video handle after you reset the video context
    /// if you are using a video handle other than the
    /// main video handle. For convenience, this function
    /// sets all video encoder back to the main video handler
    /// by default.
    ///
    /// Note that you cannot reset the graphics module
    /// without destroying the entire OBS context. Trying
    /// so will result in an error.
    pub fn reset_video(&mut self, ovi: ObsVideoInfo) -> Result<(), ObsError> {
        // You cannot change the graphics module without
        // completely destroying the entire OBS context.
        if self
            .startup_info
            .read()
            .map_err(|_| {
                ObsError::LockError("Failed to acquire read lock on startup info".to_string())
            })?
            .obs_video_info
            .graphics_module()
            != ovi.graphics_module()
        {
            return Err(ObsError::ResetVideoFailureGraphicsModule);
        }

        let has_active_outputs = {
            self.outputs
                .read()
                .map_err(|_| {
                    ObsError::LockError("Failed to acquire read lock on outputs".to_string())
                })?
                .iter()
                .any(|output| output.is_active().unwrap_or_default())
        };

        if has_active_outputs {
            return Err(ObsError::ResetVideoFailureOutputActive);
        }

        // Resets the video context. Note that this
        // is similar to Self::reset_video, but it
        // does not call that function because the
        // ObsContext struct is not created yet,
        // and also because there is no need to free
        // anything tied to the OBS context.
        let vid_ptr = Sendable(ovi.as_ptr());
        let reset_video_status = run_with_obs!(self.runtime, (vid_ptr), move || unsafe {
            // Safety: OVI is still in scope, so the pointer is valid as well.
            libobs::obs_reset_video(vid_ptr.0)
        })?;

        let reset_video_status = num_traits::FromPrimitive::from_i32(reset_video_status);

        let reset_video_status = match reset_video_status {
            Some(x) => x,
            None => ObsResetVideoStatus::Failure,
        };

        if reset_video_status == ObsResetVideoStatus::Success {
            self.startup_info
                .write()
                .map_err(|_| {
                    ObsError::LockError("Failed to acquire write lock on startup info".to_string())
                })?
                .obs_video_info = ovi;

            Ok(())
        } else {
            Err(ObsError::ResetVideoFailure(reset_video_status))
        }
    }

    /// Returns a pointer to the video output.
    ///
    /// # Safety
    /// 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.
    pub unsafe fn get_video_ptr(&self) -> Result<Sendable<*mut video_output>, ObsError> {
        // Removed safeguards here because ptr are not sendable and this OBS context should never be used across threads
        run_with_obs!(self.runtime, || unsafe {
            // Safety: This can be called as long as OBS hasn't shutdown, which it hasn't.
            Sendable(libobs::obs_get_video())
        })
    }

    /// Returns a pointer to the audio output.
    ///
    /// # Safety
    /// 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.
    pub unsafe fn get_audio_ptr(&self) -> Result<Sendable<*mut audio_output>, ObsError> {
        // Removed safeguards here because ptr are not sendable and this OBS context should never be used across threads
        run_with_obs!(self.runtime, || unsafe {
            // Safety: This can be called as long as OBS hasn't shutdown, which it hasn't.
            Sendable(libobs::obs_get_audio())
        })
    }

    pub fn data(&self) -> Result<ObsData, ObsError> {
        ObsData::new(self.runtime.clone())
    }

    pub fn replay_buffer(
        &mut self,
        info: OutputInfo,
    ) -> Result<ObsReplayBufferOutputRef, ObsError> {
        let output = ObsReplayBufferOutputRef::new(info, self.runtime.clone());

        match output {
            Ok(x) => {
                let tmp = x.clone();
                self.outputs
                    .write()
                    .map_err(|_| {
                        ObsError::LockError("Failed to acquire write lock on outputs".to_string())
                    })?
                    .push(Arc::new(Box::new(x)));
                Ok(tmp)
            }

            Err(x) => Err(x),
        }
    }

    pub fn output(&mut self, info: OutputInfo) -> Result<ObsOutputRef, ObsError> {
        let output = ObsOutputRef::new(info, self.runtime.clone());

        match output {
            Ok(x) => {
                let tmp = x.clone();
                self.outputs
                    .write()
                    .map_err(|_| {
                        ObsError::LockError("Failed to acquire write lock on outputs".to_string())
                    })?
                    .push(Arc::new(Box::new(x)));
                Ok(tmp)
            }

            Err(x) => Err(x),
        }
    }

    pub fn obs_filter(&mut self, info: FilterInfo) -> Result<ObsFilterRef, ObsError> {
        let filter = ObsFilterRef::new(
            info.id,
            info.name,
            info.settings,
            info.hotkey_data,
            self.runtime.clone(),
        );

        match filter {
            Ok(x) => {
                let tmp = x.clone();
                self.filters
                    .write()
                    .map_err(|_| {
                        ObsError::LockError("Failed to acquire write lock on filters".to_string())
                    })?
                    .push(x);
                Ok(tmp)
            }

            Err(x) => Err(x),
        }
    }

    /// Creates a new display and returns its ID.
    ///
    /// You must call `update_color_space` on the display when the window is moved, resized or the display settings change.
    ///
    /// Note: When calling `set_size` or `set_pos`, `update_color_space` is called automatically.
    ///
    /// Another note: On Linux, this method is unsafe because you must ensure that every display reference is dropped before your window exits.
    #[cfg(not(target_os = "linux"))]
    pub fn display(&mut self, data: ObsDisplayCreationData) -> Result<ObsDisplayRef, ObsError> {
        self.inner_display_fn(data)
    }

    /// Creates a new display and returns its ID.
    ///
    /// You must call `update_color_space` on the display when the window is moved, resized or the display settings change.
    ///
    /// # Safety
    /// All references of the `ObsDisplayRef` **MUST** be dropped before your window closes, otherwise you **will** have crashes.
    /// This includes calling `remove_display` or `remove_display_by_id` to remove the display from the context.
    ///
    /// 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`.
    ///
    /// Note: When calling `set_size` or `set_pos`, `update_color_space` is called automatically.
    #[cfg(target_os = "linux")]
    pub unsafe fn display(
        &mut self,
        data: ObsDisplayCreationData,
    ) -> Result<ObsDisplayRef, ObsError> {
        self.inner_display_fn(data)
    }

    /// This function is used internally to create displays.
    fn inner_display_fn(
        &mut self,
        data: ObsDisplayCreationData,
    ) -> Result<ObsDisplayRef, ObsError> {
        #[cfg(target_os = "linux")]
        {
            // 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
            // X11 allows having a separate display however.
            let nix_display = self
                .startup_info
                .read()
                .map_err(|_| {
                    ObsError::LockError("Failed to acquire read lock on startup info".to_string())
                })?
                .nix_display
                .clone();

            let is_wayland_handle = data.window_handle.is_wayland;
            if is_wayland_handle && nix_display.is_none() {
                return Err(ObsError::DisplayCreationError(
                    "Wayland window handle provided but no NixDisplay was set in StartupInfo."
                        .to_string(),
                ));
            }

            if let Some(nix_display) = &nix_display {
                if is_wayland_handle {
                    match nix_display {
                        crate::utils::NixDisplay::X11(_display) => {
                            return Err(ObsError::DisplayCreationError(
                                "Provided NixDisplay is X11, but the window handle is Wayland."
                                    .to_string(),
                            ));
                        }
                        crate::utils::NixDisplay::Wayland(display) => {
                            use crate::utils::linux::wl_proxy_get_display;
                            if !data.window_handle.is_wayland {
                                return Err(ObsError::DisplayCreationError(
                            "Provided window handle is not a Wayland handle, but the NixDisplay is Wayland.".to_string(),
                        ));
                            }

                            let surface_handle = data.window_handle.window.0.display;
                            let display_from_surface = unsafe {
                                // Safety: The display handle is valid as long as the surface is valid.
                                wl_proxy_get_display(surface_handle)
                            };
                            if let Err(e) = display_from_surface {
                                log::warn!("Could not get display from surface handle on wayland. Make sure your wayland client is at least version 1.23. Error: {:?}", e);
                            } else {
                                let display_from_surface = display_from_surface.unwrap();
                                if display_from_surface != display.0 {
                                    return Err(ObsError::DisplayCreationError(
                            "Provided surface handle's Wayland display does not match the NixDisplay's Wayland display.".to_string(),
                        ));
                                }
                            }
                        }
                    }
                }
            }
        }

        let display = ObsDisplayRef::new(data, self.runtime.clone())
            .map_err(|e| ObsError::DisplayCreationError(e.to_string()))?;

        let id = display.id();
        self.displays
            .write()
            .map_err(|_| {
                ObsError::LockError("Failed to acquire write lock on displays".to_string())
            })?
            .insert(id, display.clone());

        Ok(display)
    }

    pub fn remove_display(&mut self, display: &ObsDisplayRef) -> Result<(), ObsError> {
        self.remove_display_by_id(display.id())
    }

    pub fn remove_display_by_id(&mut self, id: usize) -> Result<(), ObsError> {
        self.displays
            .write()
            .map_err(|_| {
                ObsError::LockError("Failed to acquire write lock on displays".to_string())
            })?
            .remove(&id);

        Ok(())
    }

    pub fn get_display_by_id(&self, id: usize) -> Result<Option<ObsDisplayRef>, ObsError> {
        let d = self
            .displays
            .read()
            .map_err(|_| {
                ObsError::LockError("Failed to acquire read lock on displays".to_string())
            })?
            .get(&id)
            .cloned();

        Ok(d)
    }

    pub fn get_output(
        &mut self,
        name: &str,
    ) -> Result<Option<Arc<Box<dyn ObsOutputTrait>>>, ObsError> {
        let o = self
            .outputs
            .read()
            .map_err(|_| ObsError::LockError("Failed to acquire read lock on outputs".to_string()))?
            .iter()
            .find(|x| x.name().to_string().as_str() == name)
            .cloned();

        Ok(o)
    }

    pub fn update_output(&mut self, name: &str, settings: ObsData) -> Result<(), ObsError> {
        match self
            .outputs
            .read()
            .map_err(|_| ObsError::LockError("Failed to acquire read lock on outputs".to_string()))?
            .iter()
            .find(|x| x.name().to_string().as_str() == name)
        {
            Some(output) => output.update_settings(settings),
            None => Err(ObsError::OutputNotFound),
        }
    }

    pub fn get_filter(&mut self, name: &str) -> Result<Option<ObsFilterRef>, ObsError> {
        let f = self
            .filters
            .read()
            .map_err(|_| ObsError::LockError("Failed to acquire read lock on filters".to_string()))?
            .iter()
            .find(|x| x.name().to_string().as_str() == name)
            .cloned();

        Ok(f)
    }

    /// Creates a new scene
    ///
    /// If the channel is provided, the scene will be set to that output channel.
    ///
    /// There are 64 channels that you can assign scenes to,
    /// which will draw on top of each other in ascending index order
    /// when a output is rendered.
    ///
    /// # Arguments
    /// * `name` - The name of the scene. This must be unique.
    /// * `channel` - Optional channel to bind the scene to. If provided, the scene will be set as active for that channel.
    ///
    /// # Returns
    /// A Result containing the new ObsSceneRef or an error
    pub fn scene<T: Into<ObsString> + Send + Sync>(
        &mut self,
        name: T,
        channel: Option<u32>,
    ) -> Result<ObsSceneRef, ObsError> {
        let scene = ObsSceneRef::new(name.into(), self.runtime.clone())?;

        let tmp = scene.clone();
        self.scenes
            .write()
            .map_err(|_| ObsError::LockError("Failed to acquire write lock on scenes".to_string()))?
            .push(scene);

        if let Some(channel) = channel {
            tmp.set_to_channel(channel)?;
        }
        Ok(tmp)
    }

    pub fn get_scene(&mut self, name: &str) -> Result<Option<ObsSceneRef>, ObsError> {
        let r = self
            .scenes
            .read()
            .map_err(|_| ObsError::LockError("Failed to acquire read lock on scenes".to_string()))?
            .iter()
            .find(|x| x.name().to_string().as_str() == name)
            .cloned();

        Ok(r)
    }

    pub fn source_builder<T: ObsSourceBuilder, K: Into<ObsString> + Send + Sync>(
        &self,
        name: K,
    ) -> Result<T, ObsError> {
        T::new(name.into(), self.runtime.clone())
    }
}