1use std::{
40 collections::HashMap,
41 ffi::CStr,
42 sync::{Arc, Mutex, RwLock},
43 thread::ThreadId,
44};
45
46use crate::display::{ObsDisplayCreationData, ObsDisplayRef};
47use crate::{
48 data::{output::ObsOutputRef, video::ObsVideoInfo, ObsData},
49 enums::{ObsLogLevel, ObsResetVideoStatus},
50 logger::LOGGER,
51 run_with_obs,
52 runtime::ObsRuntime,
53 scenes::ObsSceneRef,
54 sources::{ObsFilterRef, ObsSourceBuilder},
55 unsafe_send::Sendable,
56 utils::{FilterInfo, ObsError, ObsModules, ObsString, OutputInfo, StartupInfo},
57};
58use getters0::Getters;
59use libobs::{audio_output, video_output};
60
61lazy_static::lazy_static! {
62 pub(crate) static ref OBS_THREAD_ID: Mutex<Option<ThreadId>> = Mutex::new(None);
63}
64
65#[derive(Debug, Getters, Clone)]
75#[skip_new]
76pub struct ObsContext {
77 startup_info: Arc<RwLock<StartupInfo>>,
81 #[get_mut]
82 displays: Arc<RwLock<HashMap<usize, ObsDisplayRef>>>,
84
85 #[allow(dead_code)]
88 #[get_mut]
89 pub(crate) outputs: Arc<RwLock<Vec<ObsOutputRef>>>,
90
91 #[get_mut]
92 pub(crate) scenes: Arc<RwLock<Vec<ObsSceneRef>>>,
93
94 #[get_mut]
96 pub(crate) filters: Arc<RwLock<Vec<ObsFilterRef>>>,
97
98 #[skip_getter]
99 pub(crate) active_scenes: Arc<RwLock<HashMap<u32, ObsSceneRef>>>,
101
102 #[skip_getter]
103 pub(crate) _obs_modules: Arc<ObsModules>,
104
105 pub(crate) runtime: ObsRuntime,
109
110 #[cfg(target_os = "linux")]
111 pub(crate) glib_loop: Arc<RwLock<Option<crate::utils::linux::LinuxGlibLoop>>>,
112}
113
114impl ObsContext {
115 pub fn check_version_compatibility() -> bool {
118 unsafe {
119 let version = libobs::obs_get_version_string();
120 if version.is_null() {
121 return false;
122 }
123
124 let version_str = match CStr::from_ptr(version).to_str() {
125 Ok(s) => s,
126 Err(_) => return false,
127 };
128
129 let version_parts: Vec<&str> = version_str.split('.').collect();
130 if version_parts.len() != 3 {
131 return false;
132 }
133
134 let major = match version_parts[0].parse::<u64>() {
135 Ok(v) => v,
136 Err(_) => return false,
137 };
138
139 major == libobs::LIBOBS_API_MAJOR_VER as u64
140 }
141 }
142
143 pub fn builder() -> StartupInfo {
144 StartupInfo::new()
145 }
146
147 pub fn new(info: StartupInfo) -> Result<ObsContext, ObsError> {
165 let (runtime, obs_modules, info) = ObsRuntime::startup(info)?;
167 #[cfg(target_os = "linux")]
168 let linux_opt = if info.start_glib_loop {
169 Some(crate::utils::linux::LinuxGlibLoop::new())
170 } else {
171 None
172 };
173
174 let active_scenes: Arc<RwLock<HashMap<u32, ObsSceneRef>>> = Default::default();
175 Ok(Self {
176 _obs_modules: Arc::new(obs_modules),
177 active_scenes: active_scenes.clone(),
178 displays: Default::default(),
179 outputs: Default::default(),
180 scenes: Default::default(),
181 filters: Default::default(),
182 runtime: runtime.clone(),
183 startup_info: Arc::new(RwLock::new(info)),
184 #[cfg(target_os = "linux")]
185 glib_loop: Arc::new(RwLock::new(linux_opt)),
186 })
187 }
188
189 pub fn get_version(&self) -> Result<String, ObsError> {
190 Self::get_version_global()
191 }
192
193 pub fn get_version_global() -> Result<String, ObsError> {
194 unsafe {
195 let version = libobs::obs_get_version_string();
196 let version_cstr = CStr::from_ptr(version);
197
198 let version = version_cstr.to_string_lossy().into_owned();
199
200 Ok(version)
201 }
202 }
203
204 pub fn log(&self, level: ObsLogLevel, msg: &str) {
205 let mut log = LOGGER.lock().unwrap();
206 log.log(level, msg.to_string());
207 }
208
209 pub fn reset_video(&mut self, ovi: ObsVideoInfo) -> Result<(), ObsError> {
224 if self
227 .startup_info
228 .read()
229 .map_err(|_| {
230 ObsError::LockError("Failed to acquire read lock on startup info".to_string())
231 })?
232 .obs_video_info
233 .graphics_module()
234 != ovi.graphics_module()
235 {
236 return Err(ObsError::ResetVideoFailureGraphicsModule);
237 }
238
239 let has_active_outputs = {
240 self.outputs
241 .read()
242 .map_err(|_| {
243 ObsError::LockError("Failed to acquire read lock on outputs".to_string())
244 })?
245 .iter()
246 .any(|output| output.is_active().unwrap_or_default())
247 };
248
249 if has_active_outputs {
250 return Err(ObsError::ResetVideoFailureOutputActive);
251 }
252
253 let vid_ptr = Sendable(ovi.as_ptr());
260 let reset_video_status = run_with_obs!(self.runtime, (vid_ptr), move || unsafe {
261 libobs::obs_reset_video(vid_ptr)
262 })?;
263
264 let reset_video_status = num_traits::FromPrimitive::from_i32(reset_video_status);
265
266 let reset_video_status = match reset_video_status {
267 Some(x) => x,
268 None => ObsResetVideoStatus::Failure,
269 };
270
271 if reset_video_status == ObsResetVideoStatus::Success {
272 self.startup_info
273 .write()
274 .map_err(|_| {
275 ObsError::LockError("Failed to acquire write lock on startup info".to_string())
276 })?
277 .obs_video_info = ovi;
278
279 Ok(())
280 } else {
281 Err(ObsError::ResetVideoFailure(reset_video_status))
282 }
283 }
284
285 pub unsafe fn get_video_ptr(&self) -> Result<Sendable<*mut video_output>, ObsError> {
290 run_with_obs!(self.runtime, || unsafe {
292 Sendable(libobs::obs_get_video())
293 })
294 }
295
296 pub unsafe fn get_audio_ptr(&self) -> Result<Sendable<*mut audio_output>, ObsError> {
301 run_with_obs!(self.runtime, || unsafe {
303 Sendable(libobs::obs_get_audio())
304 })
305 }
306
307 pub fn data(&self) -> Result<ObsData, ObsError> {
308 ObsData::new(self.runtime.clone())
309 }
310
311 pub fn output(&mut self, info: OutputInfo) -> Result<ObsOutputRef, ObsError> {
312 let output = ObsOutputRef::new(info, self.runtime.clone());
313
314 match output {
315 Ok(x) => {
316 let tmp = x.clone();
317 self.outputs
318 .write()
319 .map_err(|_| {
320 ObsError::LockError("Failed to acquire write lock on outputs".to_string())
321 })?
322 .push(x);
323 Ok(tmp)
324 }
325
326 Err(x) => Err(x),
327 }
328 }
329
330 pub fn obs_filter(&mut self, info: FilterInfo) -> Result<ObsFilterRef, ObsError> {
331 let filter = ObsFilterRef::new(
332 info.id,
333 info.name,
334 info.settings,
335 info.hotkey_data,
336 self.runtime.clone(),
337 );
338
339 match filter {
340 Ok(x) => {
341 let tmp = x.clone();
342 self.filters
343 .write()
344 .map_err(|_| {
345 ObsError::LockError("Failed to acquire write lock on filters".to_string())
346 })?
347 .push(x);
348 Ok(tmp)
349 }
350
351 Err(x) => Err(x),
352 }
353 }
354
355 #[cfg(not(target_os = "linux"))]
363 pub fn display(&mut self, data: ObsDisplayCreationData) -> Result<ObsDisplayRef, ObsError> {
364 self.inner_display_fn(data)
365 }
366
367 #[cfg(target_os = "linux")]
379 pub unsafe fn display(
380 &mut self,
381 data: ObsDisplayCreationData,
382 ) -> Result<ObsDisplayRef, ObsError> {
383 self.inner_display_fn(data)
384 }
385
386 fn inner_display_fn(
388 &mut self,
389 data: ObsDisplayCreationData,
390 ) -> Result<ObsDisplayRef, ObsError> {
391 #[cfg(target_os = "linux")]
392 {
393 let nix_display = self
396 .startup_info
397 .read()
398 .map_err(|_| {
399 ObsError::LockError("Failed to acquire read lock on startup info".to_string())
400 })?
401 .nix_display
402 .clone();
403
404 let is_wayland_handle = data.window_handle.is_wayland;
405 if is_wayland_handle && nix_display.is_none() {
406 return Err(ObsError::DisplayCreationError(
407 "Wayland window handle provided but no NixDisplay was set in StartupInfo."
408 .to_string(),
409 ));
410 }
411
412 if let Some(nix_display) = &nix_display {
413 if is_wayland_handle {
414 match nix_display {
415 crate::utils::NixDisplay::X11(_display) => {
416 return Err(ObsError::DisplayCreationError(
417 "Provided NixDisplay is X11, but the window handle is Wayland."
418 .to_string(),
419 ));
420 }
421 crate::utils::NixDisplay::Wayland(display) => {
422 use crate::utils::linux::wl_proxy_get_display;
423 if !data.window_handle.is_wayland {
424 return Err(ObsError::DisplayCreationError(
425 "Provided window handle is not a Wayland handle, but the NixDisplay is Wayland.".to_string(),
426 ));
427 }
428
429 let surface_handle = data.window_handle.window.0.display;
430 let display_from_surface = wl_proxy_get_display(surface_handle);
431 if let Err(e) = display_from_surface {
432 log::warn!("Could not get display from surface handle on wayland. Make sure your wayland client is at least version 1.23. Error: {:?}", e);
433 } else {
434 let display_from_surface = display_from_surface.unwrap();
435 if display_from_surface != display.0 {
436 return Err(ObsError::DisplayCreationError(
437 "Provided surface handle's Wayland display does not match the NixDisplay's Wayland display.".to_string(),
438 ));
439 }
440 }
441 }
442 }
443 }
444 }
445 }
446
447 let display = ObsDisplayRef::new(data, self.runtime.clone())
448 .map_err(|e| ObsError::DisplayCreationError(e.to_string()))?;
449
450 let id = display.id();
451 self.displays
452 .write()
453 .map_err(|_| {
454 ObsError::LockError("Failed to acquire write lock on displays".to_string())
455 })?
456 .insert(id, display.clone());
457
458 Ok(display)
459 }
460
461 pub fn remove_display(&mut self, display: &ObsDisplayRef) -> Result<(), ObsError> {
462 self.remove_display_by_id(display.id())
463 }
464
465 pub fn remove_display_by_id(&mut self, id: usize) -> Result<(), ObsError> {
466 self.displays
467 .write()
468 .map_err(|_| {
469 ObsError::LockError("Failed to acquire write lock on displays".to_string())
470 })?
471 .remove(&id);
472
473 Ok(())
474 }
475
476 pub fn get_display_by_id(&self, id: usize) -> Result<Option<ObsDisplayRef>, ObsError> {
477 let d = self
478 .displays
479 .read()
480 .map_err(|_| {
481 ObsError::LockError("Failed to acquire read lock on displays".to_string())
482 })?
483 .get(&id)
484 .cloned();
485
486 Ok(d)
487 }
488
489 pub fn get_output(&mut self, name: &str) -> Result<Option<ObsOutputRef>, ObsError> {
490 let o = self
491 .outputs
492 .read()
493 .map_err(|_| ObsError::LockError("Failed to acquire read lock on outputs".to_string()))?
494 .iter()
495 .find(|x| x.name().to_string().as_str() == name)
496 .cloned();
497
498 Ok(o)
499 }
500
501 pub fn update_output(&mut self, name: &str, settings: ObsData) -> Result<(), ObsError> {
502 match self
503 .outputs
504 .write()
505 .map_err(|_| {
506 ObsError::LockError("Failed to acquire write lock on outputs".to_string())
507 })?
508 .iter_mut()
509 .find(|x| x.name().to_string().as_str() == name)
510 {
511 Some(output) => output.update_settings(settings),
512 None => Err(ObsError::OutputNotFound),
513 }
514 }
515
516 pub fn get_filter(&mut self, name: &str) -> Result<Option<ObsFilterRef>, ObsError> {
517 let f = self
518 .filters
519 .read()
520 .map_err(|_| ObsError::LockError("Failed to acquire read lock on filters".to_string()))?
521 .iter()
522 .find(|x| x.name().to_string().as_str() == name)
523 .cloned();
524
525 Ok(f)
526 }
527
528 pub fn scene<T: Into<ObsString> + Send + Sync>(
529 &mut self,
530 name: T,
531 ) -> Result<ObsSceneRef, ObsError> {
532 let scene = ObsSceneRef::new(
533 name.into(),
534 self.active_scenes.clone(),
535 self.runtime.clone(),
536 )?;
537
538 let tmp = scene.clone();
539 self.scenes
540 .write()
541 .map_err(|_| ObsError::LockError("Failed to acquire write lock on scenes".to_string()))?
542 .push(scene);
543
544 Ok(tmp)
545 }
546
547 pub fn get_scene(&mut self, name: &str) -> Result<Option<ObsSceneRef>, ObsError> {
548 let r = self
549 .scenes
550 .read()
551 .map_err(|_| ObsError::LockError("Failed to acquire read lock on scenes".to_string()))?
552 .iter()
553 .find(|x| x.name().to_string().as_str() == name)
554 .cloned();
555 Ok(r)
556 }
557
558 pub fn source_builder<T: ObsSourceBuilder, K: Into<ObsString> + Send + Sync>(
559 &self,
560 name: K,
561 ) -> Result<T, ObsError> {
562 T::new(name.into(), self.runtime.clone())
563 }
564}