Skip to main content

libobs_wrapper/
services.rs

1//! Typed OBS streaming-service handles.
2//!
3//! Services are ordinary runtime-affine native objects: creation and mutation happen on
4//! the OBS actor, clones share one native lifetime lease, and final release is deferred
5//! back to the actor.
6
7use std::{
8    ffi::CStr,
9    sync::{Arc, RwLock},
10};
11
12use crate::{
13    data::{
14        object::{inner_fn_update_settings, ObsObjectTrait, ObsObjectTraitPrivate},
15        ImmutableObsData, ObsDataPointers,
16    },
17    impl_obs_drop,
18    macros::impl_eq_of_obs_object,
19    run_with_obs,
20    runtime::ObsRuntime,
21    unsafe_send::{Sendable, SmartPointerSendable},
22    utils::{ObjectInfo, ObsDropGuard, ObsError, ObsString},
23};
24
25#[derive(Debug)]
26struct _ObsServiceDropGuard {
27    service: Sendable<*mut libobs::obs_service_t>,
28    runtime: ObsRuntime,
29}
30
31impl ObsDropGuard for _ObsServiceDropGuard {}
32
33impl_obs_drop!(_ObsServiceDropGuard, (service), move || unsafe {
34    // Safety: The guard owns the native service reference and cleanup runs on the OBS actor.
35    libobs::obs_service_release(service.0);
36});
37
38/// A managed OBS service, such as an RTMP streaming-service configuration.
39#[derive(Clone, Debug)]
40pub struct ObsServiceRef {
41    id: ObsString,
42    name: ObsString,
43    settings: Arc<RwLock<ImmutableObsData>>,
44    hotkey_data: Arc<RwLock<ImmutableObsData>>,
45    runtime: ObsRuntime,
46    service: SmartPointerSendable<*mut libobs::obs_service_t>,
47}
48
49impl ObsServiceRef {
50    /// Returns the protocol reported by this service instance (for example `RTMP`).
51    pub fn protocol(&self) -> Result<Option<String>, ObsError> {
52        let service = self.__native_handle();
53        run_with_obs!(self.runtime, (service), move || {
54            // Safety: the managed service handle remains alive for the actor call.
55            let protocol = unsafe { libobs::obs_service_get_protocol(service.get_ptr()) };
56            if protocol.is_null() {
57                None
58            } else {
59                // Safety: libobs returns a borrowed NUL-terminated string for the service lifetime.
60                Some(
61                    unsafe { CStr::from_ptr(protocol) }
62                        .to_string_lossy()
63                        .into_owned(),
64                )
65            }
66        })
67    }
68
69    pub(crate) fn new_from_info(
70        info: ObjectInfo,
71        runtime: ObsRuntime,
72    ) -> Result<Arc<Self>, ObsError> {
73        let ObjectInfo {
74            id,
75            name,
76            settings,
77            hotkey_data,
78        } = info;
79
80        let settings_ptr = settings.as_ref().map(ObsDataPointers::as_ptr);
81        let hotkey_data_ptr = hotkey_data.as_ref().map(ObsDataPointers::as_ptr);
82
83        let service = run_with_obs!(
84            runtime,
85            (id, name, settings_ptr, hotkey_data_ptr),
86            move || {
87                let settings_raw = settings_ptr
88                    .as_ref()
89                    .map_or(std::ptr::null_mut(), SmartPointerSendable::get_ptr);
90                let hotkey_raw = hotkey_data_ptr
91                    .as_ref()
92                    .map_or(std::ptr::null_mut(), SmartPointerSendable::get_ptr);
93
94                // Safety: IDs/names stay alive for the call and data handles keep their native
95                // pointers alive while creation executes on the OBS actor.
96                let ptr = unsafe {
97                    libobs::obs_service_create(
98                        id.as_ptr().0,
99                        name.as_ptr().0,
100                        settings_raw,
101                        hotkey_raw,
102                    )
103                };
104                if ptr.is_null() {
105                    Err(ObsError::NullPointer(Some("OBS service creation".into())))
106                } else {
107                    Ok(Sendable(ptr))
108                }
109            }
110        )??;
111
112        let service = SmartPointerSendable::new(
113            service.0,
114            Arc::new(_ObsServiceDropGuard {
115                service: service.clone(),
116                runtime: runtime.clone(),
117            }),
118            runtime.native_registry(),
119        );
120
121        let current_settings = run_with_obs!(runtime, (service), move || {
122            // Safety: the managed service handle remains alive for this actor call.
123            let ptr = unsafe { libobs::obs_service_get_settings(service.get_ptr()) };
124            if ptr.is_null() {
125                Err(ObsError::NullPointer(Some("OBS service settings".into())))
126            } else {
127                Ok(Sendable(ptr))
128            }
129        })??;
130        let settings = ImmutableObsData::from_raw_pointer(current_settings, runtime.clone());
131
132        let hotkey_data = match hotkey_data {
133            Some(data) => data,
134            None => ImmutableObsData::new(&runtime)?,
135        };
136
137        Ok(Arc::new(Self {
138            id,
139            name,
140            settings: Arc::new(RwLock::new(settings)),
141            hotkey_data: Arc::new(RwLock::new(hotkey_data)),
142            runtime,
143            service,
144        }))
145    }
146}
147
148impl ObsObjectTraitPrivate for ObsServiceRef {
149    fn __internal_replace_settings(&self, settings: ImmutableObsData) -> Result<(), ObsError> {
150        *self
151            .settings
152            .write()
153            .map_err(|_| ObsError::LockError("Failed to acquire service settings lock".into()))? =
154            settings;
155        Ok(())
156    }
157
158    fn __internal_replace_hotkey_data(
159        &self,
160        hotkey_data: ImmutableObsData,
161    ) -> Result<(), ObsError> {
162        *self
163            .hotkey_data
164            .write()
165            .map_err(|_| ObsError::LockError("Failed to acquire service hotkey lock".into()))? =
166            hotkey_data;
167        Ok(())
168    }
169}
170
171impl ObsObjectTrait for ObsServiceRef {
172    type Native = *mut libobs::obs_service_t;
173
174    fn runtime(&self) -> &ObsRuntime {
175        &self.runtime
176    }
177
178    fn settings(&self) -> Result<ImmutableObsData, ObsError> {
179        self.settings
180            .read()
181            .map_err(|_| ObsError::LockError("Failed to acquire service settings lock".into()))
182            .map(|settings| settings.clone())
183    }
184
185    fn hotkey_data(&self) -> Result<ImmutableObsData, ObsError> {
186        self.hotkey_data
187            .read()
188            .map_err(|_| ObsError::LockError("Failed to acquire service hotkey lock".into()))
189            .map(|data| data.clone())
190    }
191
192    fn id(&self) -> ObsString {
193        self.id.clone()
194    }
195
196    fn name(&self) -> ObsString {
197        self.name.clone()
198    }
199
200    fn update_settings(&self, settings: crate::data::ObsData) -> Result<(), ObsError> {
201        inner_fn_update_settings!(self, libobs::obs_service_update, settings)
202    }
203
204    fn __native_handle(&self) -> SmartPointerSendable<Self::Native> {
205        self.service.clone()
206    }
207}
208
209impl_eq_of_obs_object!(ObsServiceRef);