victauri_plugin/
screencast.rs1use std::sync::Mutex;
6use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
7
8#[derive(Debug, Clone, serde::Serialize)]
10pub struct TraceFrame {
11 pub t_ms: u64,
13 pub data_b64: String,
15}
16
17#[derive(Debug)]
20pub struct Screencast {
21 active: AtomicBool,
22 interval_ms: AtomicU64,
23 max_frames: AtomicUsize,
24 generation: AtomicU64,
25 frames: Mutex<Vec<TraceFrame>>,
26 label: Mutex<Option<String>>,
27}
28
29impl Default for Screencast {
30 fn default() -> Self {
31 Self {
32 active: AtomicBool::new(false),
33 interval_ms: AtomicU64::new(500),
34 max_frames: AtomicUsize::new(60),
35 generation: AtomicU64::new(0),
36 frames: Mutex::new(Vec::new()),
37 label: Mutex::new(None),
38 }
39 }
40}
41
42impl Screencast {
43 pub fn start(&self, interval_ms: u64, max_frames: usize, label: Option<String>) -> u64 {
46 self.interval_ms
47 .store(interval_ms.max(50), Ordering::Relaxed);
48 self.max_frames
49 .store(max_frames.clamp(1, 600), Ordering::Relaxed);
50 {
51 let mut f = self
52 .frames
53 .lock()
54 .unwrap_or_else(std::sync::PoisonError::into_inner);
55 f.clear();
56 }
57 {
58 let mut l = self
59 .label
60 .lock()
61 .unwrap_or_else(std::sync::PoisonError::into_inner);
62 *l = label;
63 }
64 let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
65 self.active.store(true, Ordering::SeqCst);
66 generation
67 }
68
69 pub fn stop(&self) -> usize {
71 self.active.store(false, Ordering::SeqCst);
72 self.generation.fetch_add(1, Ordering::SeqCst);
74 self.frame_count()
75 }
76
77 #[must_use]
79 pub fn is_active(&self) -> bool {
80 self.active.load(Ordering::SeqCst)
81 }
82
83 #[must_use]
85 pub fn generation(&self) -> u64 {
86 self.generation.load(Ordering::SeqCst)
87 }
88
89 #[must_use]
91 pub fn interval_ms(&self) -> u64 {
92 self.interval_ms.load(Ordering::Relaxed)
93 }
94
95 #[must_use]
97 pub fn label(&self) -> Option<String> {
98 self.label
99 .lock()
100 .unwrap_or_else(std::sync::PoisonError::into_inner)
101 .clone()
102 }
103
104 pub fn push_frame(&self, t_ms: u64, data_b64: String) {
106 let max = self.max_frames.load(Ordering::Relaxed);
107 let mut f = self
108 .frames
109 .lock()
110 .unwrap_or_else(std::sync::PoisonError::into_inner);
111 f.push(TraceFrame { t_ms, data_b64 });
112 let len = f.len();
113 if len > max {
114 f.drain(0..len - max);
115 }
116 }
117
118 #[must_use]
120 pub fn frame_count(&self) -> usize {
121 self.frames
122 .lock()
123 .unwrap_or_else(std::sync::PoisonError::into_inner)
124 .len()
125 }
126
127 #[must_use]
129 pub fn frames(&self, limit: usize) -> Vec<TraceFrame> {
130 let f = self
131 .frames
132 .lock()
133 .unwrap_or_else(std::sync::PoisonError::into_inner);
134 if limit == 0 || limit >= f.len() {
135 f.clone()
136 } else {
137 f[f.len() - limit..].to_vec()
138 }
139 }
140
141 #[must_use]
143 pub fn frame_timestamps(&self) -> Vec<u64> {
144 self.frames
145 .lock()
146 .unwrap_or_else(std::sync::PoisonError::into_inner)
147 .iter()
148 .map(|fr| fr.t_ms)
149 .collect()
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn ring_buffer_caps_frames() {
159 let sc = Screencast::default();
160 sc.start(100, 3, None);
161 for i in 0..5 {
162 sc.push_frame(i * 100, format!("frame{i}"));
163 }
164 assert_eq!(sc.frame_count(), 3, "should cap at max_frames");
165 let frames = sc.frames(0);
166 assert_eq!(frames[0].data_b64, "frame2");
168 assert_eq!(frames[2].data_b64, "frame4");
169 }
170
171 #[test]
172 fn start_clears_and_bumps_generation() {
173 let sc = Screencast::default();
174 let g1 = sc.start(200, 10, Some("main".into()));
175 sc.push_frame(0, "x".into());
176 assert_eq!(sc.frame_count(), 1);
177 let g2 = sc.start(200, 10, None);
178 assert!(g2 > g1, "generation must increase");
179 assert_eq!(sc.frame_count(), 0, "start clears frames");
180 assert!(sc.is_active());
181 }
182
183 #[test]
184 fn stop_deactivates_and_invalidates() {
185 let sc = Screencast::default();
186 let g = sc.start(200, 10, None);
187 sc.stop();
188 assert!(!sc.is_active());
189 assert!(sc.generation() > g, "stop invalidates the task generation");
190 }
191
192 #[test]
193 fn frames_limit_returns_most_recent() {
194 let sc = Screencast::default();
195 sc.start(100, 100, None);
196 for i in 0..5 {
197 sc.push_frame(i, format!("f{i}"));
198 }
199 let last2 = sc.frames(2);
200 assert_eq!(last2.len(), 2);
201 assert_eq!(last2[0].data_b64, "f3");
202 assert_eq!(last2[1].data_b64, "f4");
203 }
204}