use std::{
collections::HashMap,
ffi::CStr,
sync::{Arc, Mutex, RwLock},
thread::ThreadId,
};
#[cfg(target_os = "linux")]
use crate::utils::initialization::PlatformType;
use crate::{
data::{
object::ObsObjectTrait,
output::{ObsOutputTrait, ObsOutputTraitSealed, ObsReplayBufferOutputRef},
},
display::{ObsDisplayCreationData, ObsDisplayRef},
};
use crate::{
data::{output::ObsOutputRef, video::ObsVideoInfo, ObsData},
enums::{ObsLogLevel, ObsResetVideoStatus},
logger::LOGGER,
run_with_obs,
runtime::ObsRuntime,
scenes::ObsSceneRef,
sources::{ObsFilterRef, ObsSourceBuilder},
unsafe_send::Sendable,
utils::{FilterInfo, ObsError, ObsModules, ObsString, OutputInfo, StartupInfo},
};
use getters0::Getters;
use libobs::{audio_output, video_output};
lazy_static::lazy_static! {
pub(crate) static ref OBS_THREAD_ID: Mutex<Option<ThreadId>> = Mutex::new(None);
}
pub(crate) type GeneralStorage<T> = Arc<RwLock<Vec<Arc<Box<T>>>>>;
#[derive(Debug, Getters, Clone)]
#[skip_new]
pub struct ObsContext {
startup_info: Arc<RwLock<StartupInfo>>,
#[get_mut]
displays: Arc<RwLock<HashMap<usize, ObsDisplayRef>>>,
#[allow(dead_code)]
#[get_mut]
outputs: GeneralStorage<dyn ObsOutputTrait>,
#[get_mut]
scenes: Arc<RwLock<Vec<ObsSceneRef>>>,
#[get_mut]
filters: Arc<RwLock<Vec<ObsFilterRef>>>,
#[skip_getter]
_obs_modules: Arc<ObsModules>,
runtime: ObsRuntime,
#[cfg(target_os = "linux")]
glib_loop: Arc<RwLock<Option<crate::utils::linux::LinuxGlibLoop>>>,
}
impl ObsContext {
pub fn check_version_compatibility() -> bool {
unsafe {
#[allow(unknown_lints)]
#[allow(ensure_obs_call_in_runtime)]
let version = libobs::obs_get_version_string();
if version.is_null() {
return false;
}
let version_str = match CStr::from_ptr(version).to_str() {
Ok(s) => s,
Err(_) => return false,
};
let version_parts: Vec<&str> = version_str.split('.').collect();
if version_parts.len() != 3 {
return false;
}
let major = match version_parts[0].parse::<u64>() {
Ok(v) => v,
Err(_) => return false,
};
major == libobs::LIBOBS_API_MAJOR_VER as u64
}
}
pub fn builder() -> StartupInfo {
StartupInfo::new()
}
pub fn new(info: StartupInfo) -> Result<ObsContext, ObsError> {
log::trace!("Getting version number...");
#[allow(unknown_lints)]
#[allow(ensure_obs_call_in_runtime)]
let version_numb = unsafe { libobs::obs_get_version() };
if version_numb == 0 {
return Err(ObsError::InvalidDll);
}
let (runtime, obs_modules, info) = ObsRuntime::startup(info)?;
#[cfg(target_os = "linux")]
let linux_opt = if info.start_glib_loop {
Some(crate::utils::linux::LinuxGlibLoop::new())
} else {
None
};
Ok(Self {
_obs_modules: Arc::new(obs_modules),
displays: Default::default(),
outputs: Default::default(),
scenes: Default::default(),
filters: Default::default(),
runtime: runtime.clone(),
startup_info: Arc::new(RwLock::new(info)),
#[cfg(target_os = "linux")]
glib_loop: Arc::new(RwLock::new(linux_opt)),
})
}
#[cfg(target_os = "linux")]
pub fn get_platform(&self) -> Result<PlatformType, ObsError> {
self.runtime.get_platform()
}
pub fn get_version(&self) -> Result<String, ObsError> {
Self::get_version_global()
}
pub fn get_version_global() -> Result<String, ObsError> {
unsafe {
#[allow(unknown_lints)]
#[allow(ensure_obs_call_in_runtime)]
let version = libobs::obs_get_version_string();
let version_cstr = CStr::from_ptr(version);
let version = version_cstr.to_string_lossy().into_owned();
Ok(version)
}
}
pub fn log(&self, level: ObsLogLevel, msg: &str) {
let mut log = LOGGER.lock().unwrap();
log.log(level, msg.to_string());
}
pub fn reset_video(&mut self, ovi: ObsVideoInfo) -> Result<(), ObsError> {
if self
.startup_info
.read()
.map_err(|_| {
ObsError::LockError("Failed to acquire read lock on startup info".to_string())
})?
.obs_video_info
.graphics_module()
!= ovi.graphics_module()
{
return Err(ObsError::ResetVideoFailureGraphicsModule);
}
let has_active_outputs = {
self.outputs
.read()
.map_err(|_| {
ObsError::LockError("Failed to acquire read lock on outputs".to_string())
})?
.iter()
.any(|output| output.is_active().unwrap_or_default())
};
if has_active_outputs {
return Err(ObsError::ResetVideoFailureOutputActive);
}
let vid_ptr = Sendable(ovi.as_ptr());
let reset_video_status = run_with_obs!(self.runtime, (vid_ptr), move || unsafe {
libobs::obs_reset_video(vid_ptr.0)
})?;
let reset_video_status = num_traits::FromPrimitive::from_i32(reset_video_status);
let reset_video_status = match reset_video_status {
Some(x) => x,
None => ObsResetVideoStatus::Failure,
};
if reset_video_status == ObsResetVideoStatus::Success {
self.startup_info
.write()
.map_err(|_| {
ObsError::LockError("Failed to acquire write lock on startup info".to_string())
})?
.obs_video_info = ovi;
Ok(())
} else {
Err(ObsError::ResetVideoFailure(reset_video_status))
}
}
pub unsafe fn get_video_ptr(&self) -> Result<Sendable<*mut video_output>, ObsError> {
run_with_obs!(self.runtime, || unsafe {
Sendable(libobs::obs_get_video())
})
}
pub unsafe fn get_audio_ptr(&self) -> Result<Sendable<*mut audio_output>, ObsError> {
run_with_obs!(self.runtime, || unsafe {
Sendable(libobs::obs_get_audio())
})
}
pub fn data(&self) -> Result<ObsData, ObsError> {
ObsData::new(self.runtime.clone())
}
pub fn replay_buffer(
&mut self,
info: OutputInfo,
) -> Result<ObsReplayBufferOutputRef, ObsError> {
let output = ObsReplayBufferOutputRef::new(info, self.runtime.clone());
match output {
Ok(x) => {
let tmp = x.clone();
self.outputs
.write()
.map_err(|_| {
ObsError::LockError("Failed to acquire write lock on outputs".to_string())
})?
.push(Arc::new(Box::new(x)));
Ok(tmp)
}
Err(x) => Err(x),
}
}
pub fn output(&mut self, info: OutputInfo) -> Result<ObsOutputRef, ObsError> {
let output = ObsOutputRef::new(info, self.runtime.clone());
match output {
Ok(x) => {
let tmp = x.clone();
self.outputs
.write()
.map_err(|_| {
ObsError::LockError("Failed to acquire write lock on outputs".to_string())
})?
.push(Arc::new(Box::new(x)));
Ok(tmp)
}
Err(x) => Err(x),
}
}
pub fn obs_filter(&mut self, info: FilterInfo) -> Result<ObsFilterRef, ObsError> {
let filter = ObsFilterRef::new(
info.id,
info.name,
info.settings,
info.hotkey_data,
self.runtime.clone(),
);
match filter {
Ok(x) => {
let tmp = x.clone();
self.filters
.write()
.map_err(|_| {
ObsError::LockError("Failed to acquire write lock on filters".to_string())
})?
.push(x);
Ok(tmp)
}
Err(x) => Err(x),
}
}
#[cfg(not(target_os = "linux"))]
pub fn display(&mut self, data: ObsDisplayCreationData) -> Result<ObsDisplayRef, ObsError> {
self.inner_display_fn(data)
}
#[cfg(target_os = "linux")]
pub unsafe fn display(
&mut self,
data: ObsDisplayCreationData,
) -> Result<ObsDisplayRef, ObsError> {
self.inner_display_fn(data)
}
fn inner_display_fn(
&mut self,
data: ObsDisplayCreationData,
) -> Result<ObsDisplayRef, ObsError> {
#[cfg(target_os = "linux")]
{
let nix_display = self
.startup_info
.read()
.map_err(|_| {
ObsError::LockError("Failed to acquire read lock on startup info".to_string())
})?
.nix_display
.clone();
let is_wayland_handle = data.window_handle.is_wayland;
if is_wayland_handle && nix_display.is_none() {
return Err(ObsError::DisplayCreationError(
"Wayland window handle provided but no NixDisplay was set in StartupInfo."
.to_string(),
));
}
if let Some(nix_display) = &nix_display {
if is_wayland_handle {
match nix_display {
crate::utils::NixDisplay::X11(_display) => {
return Err(ObsError::DisplayCreationError(
"Provided NixDisplay is X11, but the window handle is Wayland."
.to_string(),
));
}
crate::utils::NixDisplay::Wayland(display) => {
use crate::utils::linux::wl_proxy_get_display;
if !data.window_handle.is_wayland {
return Err(ObsError::DisplayCreationError(
"Provided window handle is not a Wayland handle, but the NixDisplay is Wayland.".to_string(),
));
}
let surface_handle = data.window_handle.window.0.display;
let display_from_surface = unsafe {
wl_proxy_get_display(surface_handle)
};
if let Err(e) = display_from_surface {
log::warn!("Could not get display from surface handle on wayland. Make sure your wayland client is at least version 1.23. Error: {:?}", e);
} else {
let display_from_surface = display_from_surface.unwrap();
if display_from_surface != display.0 {
return Err(ObsError::DisplayCreationError(
"Provided surface handle's Wayland display does not match the NixDisplay's Wayland display.".to_string(),
));
}
}
}
}
}
}
}
let display = ObsDisplayRef::new(data, self.runtime.clone())
.map_err(|e| ObsError::DisplayCreationError(e.to_string()))?;
let id = display.id();
self.displays
.write()
.map_err(|_| {
ObsError::LockError("Failed to acquire write lock on displays".to_string())
})?
.insert(id, display.clone());
Ok(display)
}
pub fn remove_display(&mut self, display: &ObsDisplayRef) -> Result<(), ObsError> {
self.remove_display_by_id(display.id())
}
pub fn remove_display_by_id(&mut self, id: usize) -> Result<(), ObsError> {
self.displays
.write()
.map_err(|_| {
ObsError::LockError("Failed to acquire write lock on displays".to_string())
})?
.remove(&id);
Ok(())
}
pub fn get_display_by_id(&self, id: usize) -> Result<Option<ObsDisplayRef>, ObsError> {
let d = self
.displays
.read()
.map_err(|_| {
ObsError::LockError("Failed to acquire read lock on displays".to_string())
})?
.get(&id)
.cloned();
Ok(d)
}
pub fn get_output(
&mut self,
name: &str,
) -> Result<Option<Arc<Box<dyn ObsOutputTrait>>>, ObsError> {
let o = self
.outputs
.read()
.map_err(|_| ObsError::LockError("Failed to acquire read lock on outputs".to_string()))?
.iter()
.find(|x| x.name().to_string().as_str() == name)
.cloned();
Ok(o)
}
pub fn update_output(&mut self, name: &str, settings: ObsData) -> Result<(), ObsError> {
match self
.outputs
.read()
.map_err(|_| ObsError::LockError("Failed to acquire read lock on outputs".to_string()))?
.iter()
.find(|x| x.name().to_string().as_str() == name)
{
Some(output) => output.update_settings(settings),
None => Err(ObsError::OutputNotFound),
}
}
pub fn get_filter(&mut self, name: &str) -> Result<Option<ObsFilterRef>, ObsError> {
let f = self
.filters
.read()
.map_err(|_| ObsError::LockError("Failed to acquire read lock on filters".to_string()))?
.iter()
.find(|x| x.name().to_string().as_str() == name)
.cloned();
Ok(f)
}
pub fn scene<T: Into<ObsString> + Send + Sync>(
&mut self,
name: T,
channel: Option<u32>,
) -> Result<ObsSceneRef, ObsError> {
let scene = ObsSceneRef::new(name.into(), self.runtime.clone())?;
let tmp = scene.clone();
self.scenes
.write()
.map_err(|_| ObsError::LockError("Failed to acquire write lock on scenes".to_string()))?
.push(scene);
if let Some(channel) = channel {
tmp.set_to_channel(channel)?;
}
Ok(tmp)
}
pub fn get_scene(&mut self, name: &str) -> Result<Option<ObsSceneRef>, ObsError> {
let r = self
.scenes
.read()
.map_err(|_| ObsError::LockError("Failed to acquire read lock on scenes".to_string()))?
.iter()
.find(|x| x.name().to_string().as_str() == name)
.cloned();
Ok(r)
}
pub fn source_builder<T: ObsSourceBuilder, K: Into<ObsString> + Send + Sync>(
&self,
name: K,
) -> Result<T, ObsError> {
T::new(name.into(), self.runtime.clone())
}
}