libobs_wrapper\utils/
modules.rs1use 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 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
29static 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 #[cfg(target_os = "linux")]
84 let has_disabled_module_fn = {
85 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}