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    pub fn add_paths(paths: &StartupPaths) -> Self {
34        unsafe {
35            internal_log_global(
36                ObsLogLevel::Info,
37                "[libobs-wrapper]: Adding module paths:".to_string(),
38            );
39            internal_log_global(
40                ObsLogLevel::Info,
41                format!(
42                    "[libobs-wrapper]:   libobs data path: {}",
43                    paths.libobs_data_path()
44                ),
45            );
46            internal_log_global(
47                ObsLogLevel::Info,
48                format!(
49                    "[libobs-wrapper]:   plugin bin path: {}",
50                    paths.plugin_bin_path()
51                ),
52            );
53            internal_log_global(
54                ObsLogLevel::Info,
55                format!(
56                    "[libobs-wrapper]:   plugin data path: {}",
57                    paths.plugin_data_path()
58                ),
59            );
60
61            libobs::obs_add_data_path(paths.libobs_data_path().as_ptr().0);
62            libobs::obs_add_module_path(
63                paths.plugin_bin_path().as_ptr().0,
64                paths.plugin_data_path().as_ptr().0,
65            );
66
67            #[allow(unused_mut)]
68            let mut disabled_plugins = vec!["obs-websocket", "frontend-tools"];
69
70            #[cfg(feature = "__test_environment")]
71            {
72                disabled_plugins.extend(&["decklink-output-ui", "decklink-captions", "decklink"]);
73            }
74
75            let version = ObsContext::get_version_global().unwrap_or_default();
76            let version_parts: Vec<&str> = version.split('.').collect();
77            let major = version_parts
78                .first()
79                .and_then(|s| s.parse::<u32>().ok())
80                .unwrap_or(0);
81
82            // Check if obs_add_disabled_module exists at runtime
83            #[cfg(target_os = "linux")]
84            let has_disabled_module_fn = {
85                // Try to find symbol in already loaded libraries
86                let symbol_name = CString::new("obs_add_disabled_module").unwrap();
87                let sym = libc::dlsym(libc::RTLD_DEFAULT, symbol_name.as_ptr());
88                let found = !sym.is_null();
89
90                if !found && major >= 32 {
91                    log::warn!("OBS version >= 32 but obs_add_disabled_module symbol not found, falling back to safe modules");
92                }
93
94                found
95            };
96            #[cfg(not(target_os = "linux"))]
97            let has_disabled_module_fn = major >= 32;
98
99            if major >= 32 && has_disabled_module_fn {
100                for plugin in disabled_plugins {
101                    let c_str = CString::new(plugin).unwrap();
102                    #[cfg(target_os = "linux")]
103                    {
104                        let symbol_name = CString::new("obs_add_disabled_module").unwrap();
105                        let func = libc::dlsym(libc::RTLD_DEFAULT, symbol_name.as_ptr());
106                        if !func.is_null() {
107                            let add_disabled: extern "C" fn(*const std::os::raw::c_char) =
108                                std::mem::transmute(func);
109                            add_disabled(c_str.as_ptr());
110                        }
111                    }
112                    #[cfg(not(target_os = "linux"))]
113                    {
114                        libobs::obs_add_disabled_module(c_str.as_ptr());
115                    }
116                }
117            } else {
118                for plugin in SAFE_MODULES.split('|') {
119                    if disabled_plugins.contains(&plugin) {
120                        continue;
121                    }
122                    let c_str = CString::new(plugin).unwrap();
123                    libobs::obs_add_safe_module(c_str.as_ptr());
124                }
125            }
126        }
127
128        Self {
129            paths: paths.clone(),
130            info: None,
131            runtime: None,
132        }
133    }
134
135    pub fn load_modules(&mut self) {
136        unsafe {
137            let mut failure_info: obs_module_failure_info = std::mem::zeroed();
138            internal_log_global(
139                ObsLogLevel::Info,
140                "---------------------------------".to_string(),
141            );
142            libobs::obs_load_all_modules2(&mut failure_info);
143            internal_log_global(
144                ObsLogLevel::Info,
145                "---------------------------------".to_string(),
146            );
147            libobs::obs_log_loaded_modules();
148            internal_log_global(
149                ObsLogLevel::Info,
150                "---------------------------------".to_string(),
151            );
152            libobs::obs_post_load_modules();
153            self.info = Some(Sendable(failure_info));
154        }
155
156        self.log_if_failed();
157    }
158
159    #[cfg_attr(coverage_nightly, coverage(off))]
160    pub fn log_if_failed(&self) {
161        if self.info.as_ref().is_none_or(|x| x.0.count == 0) {
162            return;
163        }
164
165        let info = &self.info.as_ref().unwrap().0;
166        let mut failed_modules = Vec::new();
167        for i in 0..info.count {
168            let module = unsafe { info.failed_modules.add(i) };
169            let plugin_name = unsafe { CStr::from_ptr(*module) };
170            failed_modules.push(plugin_name.to_string_lossy());
171        }
172
173        internal_log_global(
174            ObsLogLevel::Warning,
175            format!("Failed to load modules: {}", failed_modules.join(", ")),
176        );
177    }
178}
179
180impl Drop for ObsModules {
181    fn drop(&mut self) {
182        log::trace!("Dropping ObsModules and removing module paths...");
183
184        let paths = self.paths.clone();
185        let runtime = self.runtime.take().unwrap();
186
187        #[cfg(any(
188            not(feature = "no_blocking_drops"),
189            test,
190            feature = "__test_environment"
191        ))]
192        {
193            let r = run_with_obs!(runtime, move || unsafe {
194                libobs::obs_remove_data_path(paths.libobs_data_path().as_ptr().0);
195            });
196
197            if std::thread::panicking() {
198                return;
199            }
200
201            r.unwrap();
202        }
203
204        #[cfg(all(
205            feature = "no_blocking_drops",
206            not(test),
207            not(feature = "__test_environment")
208        ))]
209        {
210            let _ = tokio::task::spawn_blocking(move || {
211                run_with_obs!(runtime, move || unsafe {
212                    libobs::obs_remove_data_path(paths.libobs_data_path().as_ptr().0);
213                })
214                .unwrap();
215            });
216        }
217    }
218}