libobs_wrapper\utils/
modules.rs

1use std::{
2    ffi::{CStr, CString},
3    fmt::Debug,
4};
5
6use crate::{
7    context::ObsContext, enums::ObsLogLevel, logger::internal_log_global, run_with_obs,
8    runtime::ObsRuntime, unsafe_send::Sendable, utils::StartupPaths,
9};
10use libobs::obs_module_failure_info;
11
12pub struct ObsModules {
13    paths: StartupPaths,
14
15    /// A pointer to the module failure info structure.
16    info: Option<Sendable<obs_module_failure_info>>,
17    pub(crate) runtime: Option<ObsRuntime>,
18}
19
20impl Debug for ObsModules {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        f.debug_struct("ObsModules")
23            .field("paths", &self.paths)
24            .field("info", &"(internal obs_module_failure_info)")
25            .finish()
26    }
27}
28
29// List of all modules, this is for compatibility for obs versions below 32.0.0
30static SAFE_MODULES: &str = "decklink|image-source|linux-alsa|linux-capture|linux-pipewire|linux-pulseaudio|linux-v4l2|obs-ffmpeg|obs-filters|obs-nvenc|obs-outputs|obs-qsv11|obs-transitions|obs-vst|obs-websocket|obs-x264|rtmp-services|text-freetype2|vlc-video|decklink-captions|decklink-output-ui|obslua|obspython|frontend-tools";
31
32impl ObsModules {
33    /// Safety: ALWAYS CALL THIS IN THE OBS RUNTIME CONTEXT
34    #[allow(unknown_lints)]
35    #[allow(ensure_obs_call_in_runtime)]
36    pub(crate) unsafe fn add_paths(paths: &StartupPaths) -> Self {
37        internal_log_global(
38            ObsLogLevel::Info,
39            "[libobs-wrapper]: Adding module paths:".to_string(),
40        );
41        internal_log_global(
42            ObsLogLevel::Info,
43            format!(
44                "[libobs-wrapper]:   libobs data path: {}",
45                paths.libobs_data_path()
46            ),
47        );
48        internal_log_global(
49            ObsLogLevel::Info,
50            format!(
51                "[libobs-wrapper]:   plugin bin path: {}",
52                paths.plugin_bin_path()
53            ),
54        );
55        internal_log_global(
56            ObsLogLevel::Info,
57            format!(
58                "[libobs-wrapper]:   plugin data path: {}",
59                paths.plugin_data_path()
60            ),
61        );
62
63        libobs::obs_add_data_path(paths.libobs_data_path().as_ptr().0);
64        libobs::obs_add_module_path(
65            paths.plugin_bin_path().as_ptr().0,
66            paths.plugin_data_path().as_ptr().0,
67        );
68
69        #[allow(unused_mut)]
70        let mut disabled_plugins = vec!["obs-websocket", "frontend-tools"];
71
72        #[cfg(feature = "__test_environment")]
73        {
74            disabled_plugins.extend(&["decklink-output-ui", "decklink-captions", "decklink"]);
75        }
76
77        let version = ObsContext::get_version_global().unwrap_or_default();
78        let version_parts: Vec<&str> = version.split('.').collect();
79        let major = version_parts
80            .first()
81            .and_then(|s| s.parse::<u32>().ok())
82            .unwrap_or(0);
83
84        // Check if obs_add_disabled_module exists at runtime
85        #[cfg(target_os = "linux")]
86        let has_disabled_module_fn = {
87            // Try to find symbol in already loaded libraries
88            let symbol_name = CString::new("obs_add_disabled_module").unwrap();
89            let sym = libc::dlsym(libc::RTLD_DEFAULT, symbol_name.as_ptr());
90            let found = !sym.is_null();
91
92            if !found && major >= 32 {
93                log::warn!("OBS version >= 32 but obs_add_disabled_module symbol not found, falling back to safe modules");
94            }
95
96            found
97        };
98        #[cfg(not(target_os = "linux"))]
99        let has_disabled_module_fn = major >= 32;
100
101        if major >= 32 && has_disabled_module_fn {
102            for plugin in disabled_plugins {
103                let c_str = CString::new(plugin).unwrap();
104                #[cfg(target_os = "linux")]
105                {
106                    let symbol_name = CString::new("obs_add_disabled_module").unwrap();
107                    let func = libc::dlsym(libc::RTLD_DEFAULT, symbol_name.as_ptr());
108                    if !func.is_null() {
109                        let add_disabled: extern "C" fn(*const std::os::raw::c_char) =
110                            std::mem::transmute(func);
111                        add_disabled(c_str.as_ptr());
112                    }
113                }
114                #[cfg(not(target_os = "linux"))]
115                {
116                    libobs::obs_add_disabled_module(c_str.as_ptr());
117                }
118            }
119        } else {
120            for plugin in SAFE_MODULES.split('|') {
121                if disabled_plugins.contains(&plugin) {
122                    continue;
123                }
124                let c_str = CString::new(plugin).unwrap();
125                libobs::obs_add_safe_module(c_str.as_ptr());
126            }
127        }
128
129        Self {
130            paths: paths.clone(),
131            info: None,
132            runtime: None,
133        }
134    }
135
136    /// Safety: ALWAYS CALL THIS IN THE OBS RUNTIME CONTEXT
137    #[allow(unknown_lints)]
138    #[allow(ensure_obs_call_in_runtime)]
139    pub(crate) unsafe fn load_modules(&mut self) {
140        let mut failure_info: obs_module_failure_info = std::mem::zeroed();
141        internal_log_global(
142            ObsLogLevel::Info,
143            "---------------------------------".to_string(),
144        );
145        libobs::obs_load_all_modules2(&mut failure_info);
146        internal_log_global(
147            ObsLogLevel::Info,
148            "---------------------------------".to_string(),
149        );
150        libobs::obs_log_loaded_modules();
151        internal_log_global(
152            ObsLogLevel::Info,
153            "---------------------------------".to_string(),
154        );
155        libobs::obs_post_load_modules();
156        self.info = Some(Sendable(failure_info));
157
158        self.log_if_failed();
159    }
160
161    #[cfg_attr(coverage_nightly, coverage(off))]
162    #[allow(unknown_lints)]
163    #[allow(ensure_obs_call_in_runtime)]
164    unsafe fn log_if_failed(&self) {
165        if self.info.as_ref().is_none_or(|x| x.0.count == 0) {
166            return;
167        }
168
169        let info = &self.info.as_ref().unwrap().0;
170        let mut failed_modules = Vec::new();
171        for i in 0..info.count {
172            let module = info.failed_modules.add(i);
173            let plugin_name = CStr::from_ptr(*module);
174            failed_modules.push(plugin_name.to_string_lossy());
175        }
176
177        internal_log_global(
178            ObsLogLevel::Warning,
179            format!("Failed to load modules: {}", failed_modules.join(", ")),
180        );
181    }
182}
183
184impl Drop for ObsModules {
185    fn drop(&mut self) {
186        log::trace!("Dropping ObsModules and removing module paths...");
187
188        let paths = self.paths.clone();
189        let runtime = self.runtime.take().unwrap();
190
191        #[cfg(any(
192            not(feature = "no_blocking_drops"),
193            test,
194            feature = "__test_environment",
195            not(feature = "enable_runtime")
196        ))]
197        {
198            let data_path = paths.libobs_data_path().clone();
199            let r = run_with_obs!(runtime, (data_path), move || unsafe {
200                // Safety: This is running in the OBS thread, so it's safe to call this here and the pointer is valid.
201                libobs::obs_remove_data_path(data_path.as_ptr().0);
202            });
203
204            if std::thread::panicking() {
205                return;
206            }
207
208            r.unwrap();
209        }
210
211        #[cfg(all(
212            feature = "no_blocking_drops",
213            not(test),
214            not(feature = "__test_environment"),
215            feature = "enable_runtime"
216        ))]
217        {
218            let _ = tokio::task::spawn_blocking(move || {
219                run_with_obs!(runtime, move || unsafe {
220                    libobs::obs_remove_data_path(paths.libobs_data_path().as_ptr().0);
221                })
222                .unwrap();
223            });
224        }
225    }
226}