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, runtime::ObsRuntime,
8 utils::StartupPaths,
9};
10use libobs::obs_module_failure_info;
11
12pub struct ObsModules {
13 paths: StartupPaths,
14
15 failed_modules: Vec<String>,
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("failed_modules", &self.failed_modules)
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 #[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 #[cfg(target_os = "linux")]
86 let has_disabled_module_fn = {
87 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 failed_modules: Vec::new(),
132 runtime: None,
133 }
134 }
135
136 #[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.failed_modules.clear();
157 for i in 0..failure_info.count {
158 let module = failure_info.failed_modules.add(i);
159 if !module.is_null() && !(*module).is_null() {
160 self.failed_modules
161 .push(CStr::from_ptr(*module).to_string_lossy().into_owned());
162 }
163 }
164 libobs::obs_module_failure_info_free(&mut failure_info);
165
166 self.log_if_failed();
167 }
168
169 #[cfg_attr(coverage_nightly, coverage(off))]
170 #[allow(unknown_lints)]
171 #[allow(ensure_obs_call_in_runtime)]
172 unsafe fn log_if_failed(&self) {
173 if self.failed_modules.is_empty() {
174 return;
175 }
176
177 internal_log_global(
178 ObsLogLevel::Warning,
179 format!("Failed to load modules: {}", self.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 let Some(runtime) = self.runtime.take() else {
188 return;
189 };
190 let data_path = self.paths.libobs_data_path().clone();
191 let cleanup_runtime = runtime.clone();
192 runtime.defer_obs_cleanup(move || {
193 let result = cleanup_runtime.run_with_obs_result(move || unsafe {
194 libobs::obs_remove_data_path(data_path.as_ptr().0);
196 });
197 if let Err(err) = result {
198 log::error!(
199 "Failed to remove the OBS data path during cleanup: {:?}",
200 err
201 );
202 }
203 });
204 }
205}