pixelflux/
recording_sink.rs1use std::fs;
14use std::io::{ErrorKind, Write};
15use std::os::unix::net::UnixListener;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::{Arc, Mutex};
18use std::thread;
19use std::time::Duration;
20
21use crossbeam_channel::{bounded, Sender, TrySendError};
22
23use crate::encoders::software::EncodedStripe;
24
25const WRITE_TIMEOUT: Duration = Duration::from_millis(100);
28
29const ACCEPT_POLL_INTERVAL: Duration = Duration::from_millis(50);
31
32const CLIENT_QUEUE_CAP: usize = 256;
35
36type QueuedFrame = (Arc<Vec<u8>>, usize);
39
40struct ClientHandle {
43 tx: Sender<QueuedFrame>,
44 stop: Arc<AtomicBool>,
45}
46
47pub struct RecordingSink {
52 path: String,
54 clients: Arc<Mutex<Vec<ClientHandle>>>,
56 shutdown: Arc<AtomicBool>,
58 client_connected: Arc<AtomicBool>,
62 warned_unrecordable: AtomicBool,
64}
65
66impl RecordingSink {
67 pub fn try_bind(settings_path: &str) -> Option<Arc<Self>> {
71 if settings_path.is_empty() {
72 return None;
73 }
74 match Self::bind(settings_path.to_string()) {
75 Ok(sink) => Some(Arc::new(sink)),
76 Err(e) => {
77 eprintln!("[recording_sink] bind failed: {:?}", e);
78 None
79 }
80 }
81 }
82
83 fn bind(path: String) -> std::io::Result<Self> {
86 let _ = fs::remove_file(&path);
87 let listener = UnixListener::bind(&path)?;
88 listener.set_nonblocking(true)?;
89
90 let clients: Arc<Mutex<Vec<ClientHandle>>> = Arc::new(Mutex::new(Vec::new()));
91 let shutdown = Arc::new(AtomicBool::new(false));
92 let client_connected = Arc::new(AtomicBool::new(false));
93
94 let clients_acc = clients.clone();
95 let shutdown_acc = shutdown.clone();
96 let client_connected_acc = client_connected.clone();
97 let path_log = path.clone();
98
99 thread::spawn(move || {
100 eprintln!("[recording_sink] listening on {}", path_log);
101 while !shutdown_acc.load(Ordering::Relaxed) {
102 match listener.accept() {
103 Ok((stream, _)) => {
104 if let Err(e) = stream.set_write_timeout(Some(WRITE_TIMEOUT)) {
105 eprintln!("[recording_sink] set_write_timeout failed: {:?}", e);
106 continue;
107 }
108
109 let (tx, rx) = bounded::<QueuedFrame>(CLIENT_QUEUE_CAP);
110 let stop = Arc::new(AtomicBool::new(false));
111 let stop_writer = stop.clone();
112 thread::spawn(move || {
113 let mut stream = stream;
114 for (buf, offset) in rx.iter() {
115 if stop_writer.load(Ordering::Relaxed) {
116 break;
117 }
118 if let Err(e) =
119 write_all_frame(&mut stream, &buf[offset..], &stop_writer)
120 {
121 eprintln!(
122 "[recording_sink] writer thread exiting; write failed: {:?}",
123 e
124 );
125 break;
126 }
127 }
128 });
129
130 let mut guard = clients_acc.lock().unwrap();
131 guard.push(ClientHandle { tx, stop });
132 client_connected_acc.store(true, Ordering::Relaxed);
133 eprintln!("[recording_sink] client connected; total {}", guard.len());
134 }
135 Err(e) if e.kind() == ErrorKind::WouldBlock => {
136 thread::sleep(ACCEPT_POLL_INTERVAL);
137 }
138 Err(e) => {
139 eprintln!("[recording_sink] accept error: {:?}", e);
140 thread::sleep(Duration::from_millis(500));
141 }
142 }
143 }
144 eprintln!("[recording_sink] listener thread exiting");
145 });
146
147 Ok(Self {
148 path,
149 clients,
150 shutdown,
151 client_connected,
152 warned_unrecordable: AtomicBool::new(false),
153 })
154 }
155
156 pub fn should_force_idr(&self) -> bool {
159 self.client_connected.swap(false, Ordering::Relaxed)
160 }
161
162 pub fn write_frame(&self, stripes: &[EncodedStripe], full_height: i32) {
174 let mut h264 = stripes
175 .iter()
176 .filter(|s| s.data_type == 2 && !s.data.is_empty());
177 let Some(stripe) = h264.next() else { return };
178 if h264.next().is_some() || stripe.stripe_y_start != 0 || stripe.stripe_height != full_height
179 {
180 if !self.warned_unrecordable.swap(true, Ordering::Relaxed) {
181 eprintln!(
182 "[recording_sink] WARNING: striped H.264 frames are not recordable \
183 (the socket carries one elementary stream); use a full-frame encoder \
184 to record this session"
185 );
186 }
187 return;
188 }
189 let offset = if stripe.data.len() >= 10 && stripe.data[0] == 0x04 {
190 10
191 } else {
192 0
193 };
194 if stripe.data.len() == offset {
195 return;
196 }
197
198 let mut clients = self.clients.lock().unwrap();
199 if clients.is_empty() {
200 return;
201 }
202 let mut to_remove: Vec<usize> = Vec::new();
203 for (idx, client) in clients.iter().enumerate() {
204 match client.tx.try_send((stripe.data.clone(), offset)) {
205 Ok(()) => {}
206 Err(TrySendError::Full(_)) => {
207 eprintln!("[recording_sink] dropping slow client (idx {})", idx);
208 to_remove.push(idx);
209 }
210 Err(TrySendError::Disconnected(_)) => {
211 to_remove.push(idx);
212 }
213 }
214 }
215 for idx in to_remove.into_iter().rev() {
216 let removed = clients.swap_remove(idx);
217 removed.stop.store(true, Ordering::Relaxed);
218 }
219 }
220}
221
222impl Drop for RecordingSink {
223 fn drop(&mut self) {
226 self.shutdown.store(true, Ordering::Relaxed);
227 if let Ok(mut clients) = self.clients.lock() {
228 for client in clients.iter() {
229 client.stop.store(true, Ordering::Relaxed);
230 }
231 clients.clear();
232 }
233 let _ = fs::remove_file(&self.path);
234 }
235}
236
237fn write_all_frame<W: Write>(stream: &mut W, buf: &[u8], stop: &AtomicBool) -> std::io::Result<()> {
241 let mut written = 0usize;
242 while written < buf.len() {
243 if stop.load(Ordering::Relaxed) {
244 return Err(std::io::Error::other("writer stopped (client dropped)"));
245 }
246 match stream.write(&buf[written..]) {
247 Ok(0) => {
248 return Err(std::io::Error::new(
249 ErrorKind::WriteZero,
250 "failed to write whole frame",
251 ));
252 }
253 Ok(n) => written += n,
254 Err(ref e) if e.kind() == ErrorKind::TimedOut => {}
255 Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
256 thread::sleep(Duration::from_millis(5));
257 }
258 Err(ref e) if e.kind() == ErrorKind::Interrupted => {}
259 Err(e) => return Err(e),
260 }
261 }
262 Ok(())
263}
264
265#[cfg(test)]
266mod cost_tests {
267 use super::*;
274 use std::io::Read;
275 use std::os::unix::net::UnixStream;
276 use std::time::Instant;
277
278 fn frame(len: usize) -> EncodedStripe {
279 let mut data = vec![0u8; len];
280 data[0] = 0x04; EncodedStripe {
282 data: Arc::new(data),
283 data_type: 2,
284 stripe_y_start: 0,
285 stripe_height: 720,
286 frame_id: 0,
287 }
288 }
289
290 fn feed_timed(sink: &RecordingSink, n: usize, len: usize) -> (f64, f64) {
291 let f = frame(len);
292 let mut max_us = 0f64;
293 let mut total_us = 0f64;
294 for _ in 0..n {
295 let t = Instant::now();
296 sink.write_frame(std::slice::from_ref(&f), 720);
297 let us = t.elapsed().as_secs_f64() * 1e6;
298 total_us += us;
299 max_us = max_us.max(us);
300 thread::sleep(Duration::from_micros(200));
301 }
302 (total_us / n as f64, max_us)
303 }
304
305 #[test]
306 fn stalled_recorder_isolation_cost() {
307 let path = format!("/tmp/pf-sink-cost-{}.sock", std::process::id());
308 let sink = RecordingSink::try_bind(&path).expect("bind");
309
310 let (idle_mean, idle_max) = feed_timed(&sink, 500, 100_000);
312
313 let mut healthy = UnixStream::connect(&path).expect("connect");
315 thread::sleep(Duration::from_millis(200));
316 let drain = thread::spawn(move || {
317 let mut buf = vec![0u8; 1 << 20];
318 while healthy.read(&mut buf).map(|n| n > 0).unwrap_or(false) {}
319 });
320 let (healthy_mean, healthy_max) = feed_timed(&sink, 500, 100_000);
321
322 let stalled = UnixStream::connect(&path).expect("connect");
325 thread::sleep(Duration::from_millis(200));
326 let (stalled_mean, stalled_max) = feed_timed(&sink, 500, 100_000);
327 drop(stalled);
328
329 println!(
330 "[sink-cost] idle mean {idle_mean:.3}us max {idle_max:.3}us\n\
331 [sink-cost] healthy mean {healthy_mean:.3}us max {healthy_max:.3}us\n\
332 [sink-cost] stalled mean {stalled_mean:.3}us max {stalled_max:.3}us"
333 );
334 drop(sink);
335 let _ = drain.join();
336
337 assert!(idle_mean < 5.0, "idle feed should be sub-5us, was {idle_mean:.3}us");
338 assert!(healthy_mean < 100.0, "healthy feed should be tens of us, was {healthy_mean:.3}us");
339 assert!(
340 stalled_max < 10_000.0,
341 "a stalled recorder must never block the tap >10ms, was {stalled_max:.3}us"
342 );
343 }
344}