1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use arc_swap::ArcSwapOption;
5use tokio::sync::{broadcast, watch};
6use tokio::task::JoinHandle;
7use tokio::time::MissedTickBehavior;
8
9use std::time::SystemTime;
10
11use crate::error::{Error, Result};
12use crate::event::{Event, EventKind};
13use visual_cortex_capture::{CaptureError, Frame, FrameSource, Rate, Target};
14
15const BROADCAST_CAPACITY: usize = 256;
16const INITIAL_CAPTURE_RATE_HZ: f64 = 1.0;
17const SESSION_WATCHER: &str = "$session";
19
20#[derive(Clone, Debug)]
23pub struct Session {
24 pub(crate) inner: Arc<Inner>,
25}
26
27#[derive(Debug)]
28pub(crate) struct Inner {
29 pub(crate) slot: Arc<ArcSwapOption<Frame>>,
30 pub(crate) events: broadcast::Sender<Event>,
31 pub(crate) rate_tx: watch::Sender<Rate>,
32 pub(crate) watchers: Mutex<HashMap<String, WatcherEntry>>,
33 pub(crate) capture_task: Mutex<Option<JoinHandle<()>>>,
34}
35
36#[derive(Debug)]
40pub(crate) struct WatcherEntry {
41 pub(crate) rate: Rate,
42 pub(crate) task: Option<JoinHandle<()>>,
43}
44
45impl Drop for Inner {
46 fn drop(&mut self) {
47 if let Some(task) = self.capture_task.lock().unwrap().take() {
48 task.abort();
49 }
50 for (_, mut entry) in self.watchers.lock().unwrap().drain() {
51 if let Some(task) = entry.task.take() {
52 task.abort();
53 }
54 }
55 }
56}
57
58impl Session {
59 pub fn builder() -> SessionBuilder {
60 SessionBuilder {
61 source: None,
62 target: None,
63 reattach: true,
64 }
65 }
66
67 pub fn watch(&self, name: &str) -> crate::watcher::WatcherBuilder<'_> {
70 crate::watcher::WatcherBuilder::new(self, name)
71 }
72
73 pub fn unsubscribe(&self, name: &str) -> bool {
77 let entry = self.inner.watchers.lock().unwrap().remove(name);
78 match entry {
79 Some(mut entry) => {
80 if let Some(task) = entry.task.take() {
81 task.abort();
82 }
83 recompute_capture_rate(&self.inner);
84 true
85 }
86 None => false,
87 }
88 }
89}
90
91pub struct SessionBuilder {
92 source: Option<Box<dyn FrameSource>>,
93 target: Option<Target>,
94 reattach: bool,
95}
96
97impl SessionBuilder {
98 pub fn source(mut self, source: impl FrameSource) -> Self {
101 self.source = Some(Box::new(source));
102 self
103 }
104
105 pub fn target(mut self, target: Target) -> Self {
108 self.target = Some(target);
109 self
110 }
111
112 pub fn reattach(mut self, reattach: bool) -> Self {
114 self.reattach = reattach;
115 self
116 }
117
118 pub async fn build(self) -> Result<Session> {
119 let source = self.resolve_source()?;
120 let slot = Arc::new(ArcSwapOption::from(None));
121 let (events, _) = broadcast::channel(BROADCAST_CAPACITY);
122 let (rate_tx, rate_rx) = watch::channel(Rate::hz(INITIAL_CAPTURE_RATE_HZ));
123 let capture_task =
124 tokio::spawn(capture_loop(source, slot.clone(), rate_rx, events.clone()));
125 Ok(Session {
126 inner: Arc::new(Inner {
127 slot,
128 events,
129 rate_tx,
130 watchers: Mutex::new(HashMap::new()),
131 capture_task: Mutex::new(Some(capture_task)),
132 }),
133 })
134 }
135
136 fn resolve_source(self) -> Result<Box<dyn FrameSource>> {
137 match (self.source, self.target) {
138 (Some(_), Some(_)) => Err(Error::AmbiguousSource),
139 (Some(source), None) => Ok(source),
140 (None, Some(target)) => build_target_source(&target, self.reattach),
141 (None, None) => Err(Error::NoSource),
142 }
143 }
144}
145
146#[cfg(target_os = "macos")]
147fn build_target_source(target: &Target, reattach: bool) -> Result<Box<dyn FrameSource>> {
148 Ok(Box::new(visual_cortex_capture::ScapSource::new(
149 target, reattach,
150 )?))
151}
152
153#[cfg(not(target_os = "macos"))]
154fn build_target_source(_target: &Target, _reattach: bool) -> Result<Box<dyn FrameSource>> {
155 Err(Error::UnsupportedPlatform)
156}
157
158pub(crate) fn recompute_capture_rate(inner: &Inner) {
161 let fastest = {
162 let watchers = inner.watchers.lock().unwrap();
163 watchers
164 .values()
165 .map(|entry| entry.rate)
166 .min_by_key(|rate| rate.period())
168 .unwrap_or_else(|| Rate::hz(INITIAL_CAPTURE_RATE_HZ))
169 };
170 if fastest != *inner.rate_tx.borrow() {
172 let _ = inner.rate_tx.send(fastest);
173 }
174}
175
176fn send_session_event(events: &broadcast::Sender<Event>, kind: EventKind) {
179 let _ = events.send(Event {
180 watcher: Arc::from(SESSION_WATCHER),
181 timestamp: SystemTime::now(),
182 kind,
183 });
184}
185
186async fn capture_loop(
190 mut source: Box<dyn FrameSource>,
191 slot: Arc<ArcSwapOption<Frame>>,
192 mut rate_rx: watch::Receiver<Rate>,
193 events: broadcast::Sender<Event>,
194) {
195 let mut interval = tokio::time::interval(rate_rx.borrow().period());
196 interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
197 let mut target_present = true;
199 loop {
200 tokio::select! {
201 _ = interval.tick() => {
202 match source.capture_frame().await {
203 Ok(frame) => {
204 if !target_present {
205 target_present = true;
206 send_session_event(&events, EventKind::TargetReattached);
207 }
208 slot.store(Some(frame));
209 }
210 Err(CaptureError::TargetLost) | Err(CaptureError::TargetNotFound(_)) => {
211 if target_present {
212 target_present = false;
213 send_session_event(&events, EventKind::TargetLost);
214 }
215 }
216 Err(e) => tracing::warn!("capture error: {e}"),
217 }
218 }
219 changed = rate_rx.changed() => {
220 if changed.is_err() {
221 break; }
223 interval = tokio::time::interval(rate_rx.borrow().period());
226 interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
227 }
228 }
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use visual_cortex_capture::FakeSource;
236
237 #[tokio::test]
238 async fn build_without_source_errors() {
239 let err = Session::builder().build().await.unwrap_err();
240 assert!(matches!(err, Error::NoSource));
241 }
242
243 #[tokio::test(start_paused = true)]
244 async fn capture_loop_fills_the_frame_slot() {
245 let session = Session::builder()
246 .source(FakeSource::new([Frame::solid(4, 4, [1, 2, 3, 255])]))
247 .build()
248 .await
249 .unwrap();
250 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
252 let frame = session
253 .inner
254 .slot
255 .load_full()
256 .expect("slot should hold a frame");
257 assert_eq!(frame.data()[0], 1);
258 }
259
260 use visual_cortex_vision::Brightness;
261
262 fn one_frame_session_source() -> FakeSource {
263 FakeSource::new([Frame::solid(4, 4, [0, 0, 0, 255])])
264 }
265
266 #[tokio::test(start_paused = true)]
267 async fn capture_rate_tracks_fastest_watcher_and_recovers_on_unsubscribe() {
268 let session = Session::builder()
269 .source(one_frame_session_source())
270 .build()
271 .await
272 .unwrap();
273
274 let _slow = session
275 .watch("slow")
276 .rate(Rate::hz(2.0))
277 .detector(Brightness)
278 .subscribe()
279 .unwrap();
280 assert_eq!(*session.inner.rate_tx.borrow(), Rate::hz(2.0));
281
282 let _fast = session
283 .watch("fast")
284 .rate(Rate::hz(10.0))
285 .detector(Brightness)
286 .subscribe()
287 .unwrap();
288 assert_eq!(*session.inner.rate_tx.borrow(), Rate::hz(10.0));
289
290 assert!(session.unsubscribe("fast"));
292 assert_eq!(*session.inner.rate_tx.borrow(), Rate::hz(2.0));
293
294 assert!(session.unsubscribe("slow"));
296 assert_eq!(
297 *session.inner.rate_tx.borrow(),
298 Rate::hz(INITIAL_CAPTURE_RATE_HZ)
299 );
300 }
301
302 #[tokio::test]
303 async fn build_rejects_source_and_target_together() {
304 use visual_cortex_capture::Target;
305 let err = Session::builder()
306 .source(FakeSource::new([Frame::solid(2, 2, [0, 0, 0, 255])]))
307 .target(Target::display(0))
308 .build()
309 .await
310 .unwrap_err();
311 assert!(matches!(err, Error::AmbiguousSource));
312 }
313
314 #[tokio::test(start_paused = true)]
315 async fn unsubscribe_unknown_is_false_and_frees_the_name() {
316 let session = Session::builder()
317 .source(one_frame_session_source())
318 .build()
319 .await
320 .unwrap();
321
322 assert!(!session.unsubscribe("ghost"));
323
324 let _w = session.watch("w").detector(Brightness).subscribe().unwrap();
325 let dup = session.watch("w").detector(Brightness).subscribe();
326 assert!(matches!(dup, Err(Error::DuplicateWatcher(n)) if n == "w"));
327
328 assert!(session.unsubscribe("w"));
330 let _w2 = session.watch("w").detector(Brightness).subscribe().unwrap();
331 }
332}