1use std::fs;
16use std::io::{ErrorKind, Write};
17use std::os::fd::AsRawFd;
18use std::os::unix::net::UnixListener;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::mpsc::{SyncSender, TrySendError, sync_channel};
21use std::sync::{Arc, Mutex};
22use std::thread;
23use std::time::Duration;
24
25use crate::encoders::codec::{Codec, VIDEO_HEADER_LEN, WIRE_VIDEO};
26use crate::encoders::frame_rate::FrameRate;
27use crate::encoders::software::EncodedStripe;
28
29const WRITE_TIMEOUT: Duration = Duration::from_millis(100);
32
33const ACCEPT_POLL_INTERVAL: Duration = Duration::from_millis(50);
35
36const CLIENT_QUEUE_CAP: usize = 256;
39
40struct QueuedFrame {
45 prefix: Vec<u8>,
46 #[expect(
47 clippy::rc_buffer,
48 reason = "the encoder's buffer as it hands it over; an `Arc<[u8]>` would copy every frame"
49 )]
50 data: Arc<Vec<u8>>,
51 offset: usize,
52}
53
54struct ClientHandle {
58 tx: SyncSender<QueuedFrame>,
59 stop: Arc<AtomicBool>,
60 frames: u64,
61}
62
63pub struct RecordingSink {
68 path: String,
70 clients: Arc<Mutex<Vec<ClientHandle>>>,
72 shutdown: Arc<AtomicBool>,
74 client_connected: Arc<AtomicBool>,
78 fps: FrameRate,
80}
81
82impl RecordingSink {
83 pub fn try_bind(settings_path: &str, fps: f64) -> Option<Arc<Self>> {
87 if settings_path.is_empty() {
88 return None;
89 }
90 match Self::bind(settings_path.to_string(), FrameRate::of(fps)) {
91 Ok(sink) => Some(Arc::new(sink)),
92 Err(e) => {
93 eprintln!("[recording_sink] bind failed: {:?}", e);
94 None
95 }
96 }
97 }
98
99 fn bind(path: String, fps: FrameRate) -> std::io::Result<Self> {
102 let _ = fs::remove_file(&path);
103 let listener = UnixListener::bind(&path)?;
104 crate::webcam::server::restrict_to_owner(&path)?;
105 listener.set_nonblocking(true)?;
106
107 let clients: Arc<Mutex<Vec<ClientHandle>>> = Arc::new(Mutex::new(Vec::new()));
108 let shutdown = Arc::new(AtomicBool::new(false));
109 let client_connected = Arc::new(AtomicBool::new(false));
110
111 let clients_acc = clients.clone();
112 let shutdown_acc = shutdown.clone();
113 let client_connected_acc = client_connected.clone();
114 let path_log = path.clone();
115
116 thread::spawn(move || {
117 eprintln!("[recording_sink] listening on {}", path_log);
118 while !shutdown_acc.load(Ordering::Relaxed) {
119 match listener.accept() {
120 Ok((stream, _)) => {
121 if !crate::webcam::server::peer_is_session_user(stream.as_raw_fd()) {
122 continue;
123 }
124 if let Err(e) = stream.set_write_timeout(Some(WRITE_TIMEOUT)) {
125 eprintln!("[recording_sink] set_write_timeout failed: {:?}", e);
126 continue;
127 }
128
129 let (tx, rx) = sync_channel::<QueuedFrame>(CLIENT_QUEUE_CAP);
130 let stop = Arc::new(AtomicBool::new(false));
131 let stop_writer = stop.clone();
132 thread::spawn(move || {
133 let mut stream = stream;
134 for QueuedFrame {
135 prefix,
136 data,
137 offset,
138 } in rx.iter()
139 {
140 if stop_writer.load(Ordering::Relaxed) {
141 break;
142 }
143 let written = if prefix.is_empty() {
144 write_all_frame(&mut stream, &data[offset..], &stop_writer)
145 } else {
146 write_all_frame(&mut stream, &prefix, &stop_writer).and_then(
147 |()| {
148 write_all_frame(
149 &mut stream,
150 &data[offset..],
151 &stop_writer,
152 )
153 },
154 )
155 };
156 if let Err(e) = written {
157 eprintln!(
158 "[recording_sink] writer thread exiting; write failed: {:?}",
159 e
160 );
161 break;
162 }
163 }
164 });
165
166 let mut guard = clients_acc.lock().unwrap();
167 guard.push(ClientHandle {
168 tx,
169 stop,
170 frames: 0,
171 });
172 client_connected_acc.store(true, Ordering::Relaxed);
173 eprintln!("[recording_sink] client connected; total {}", guard.len());
174 }
175 Err(e) if e.kind() == ErrorKind::WouldBlock => {
176 thread::sleep(ACCEPT_POLL_INTERVAL);
177 }
178 Err(e) => {
179 eprintln!("[recording_sink] accept error: {:?}", e);
180 thread::sleep(Duration::from_millis(500));
181 }
182 }
183 }
184 eprintln!("[recording_sink] listener thread exiting");
185 });
186
187 Ok(Self {
188 path,
189 clients,
190 shutdown,
191 client_connected,
192 fps,
193 })
194 }
195
196 pub fn should_force_idr(&self) -> bool {
199 self.client_connected.swap(false, Ordering::Relaxed)
200 }
201
202 pub fn write_frame(&self, stripes: &[EncodedStripe], full_width: i32, full_height: i32) {
211 let Some(stripe) = stripes
212 .iter()
213 .find(|s| s.codec.is_video() && !s.data.is_empty())
214 else {
215 return;
216 };
217 let offset = if stripe.data.len() >= VIDEO_HEADER_LEN && stripe.data[0] == WIRE_VIDEO {
218 VIDEO_HEADER_LEN
219 } else {
220 0
221 };
222 if stripe.data.len() == offset {
223 return;
224 }
225 let payload = &stripe.data[offset..];
226
227 let mut clients = self.clients.lock().unwrap();
228 if clients.is_empty() {
229 return;
230 }
231 let mut to_remove: Vec<usize> = Vec::new();
232 for (idx, client) in clients.iter_mut().enumerate() {
233 let prefix = stream_prefix(
234 stripe.codec,
235 payload,
236 client.frames,
237 (full_width as u16, full_height as u16),
238 self.fps,
239 );
240 client.frames += 1;
241 let queued = QueuedFrame {
242 prefix,
243 data: stripe.data.clone(),
244 offset,
245 };
246 match client.tx.try_send(queued) {
247 Ok(()) => {}
248 Err(TrySendError::Full(_)) => {
249 eprintln!("[recording_sink] dropping slow client (idx {})", idx);
250 to_remove.push(idx);
251 }
252 Err(TrySendError::Disconnected(_)) => {
253 to_remove.push(idx);
254 }
255 }
256 }
257 for idx in to_remove.into_iter().rev() {
258 let removed = clients.swap_remove(idx);
259 removed.stop.store(true, Ordering::Relaxed);
260 }
261 }
262}
263
264impl Drop for RecordingSink {
265 fn drop(&mut self) {
268 self.shutdown.store(true, Ordering::Relaxed);
269 if let Ok(mut clients) = self.clients.lock() {
270 for client in clients.iter() {
271 client.stop.store(true, Ordering::Relaxed);
272 }
273 clients.clear();
274 }
275 let _ = fs::remove_file(&self.path);
276 }
277}
278
279fn stream_prefix(
284 codec: Codec,
285 payload: &[u8],
286 index: u64,
287 size: (u16, u16),
288 fps: FrameRate,
289) -> Vec<u8> {
290 match codec {
291 Codec::Vp8 | Codec::Vp9 => {
292 let mut prefix = Vec::with_capacity(44);
293 if index == 0 {
294 prefix.extend_from_slice(b"DKIF");
295 prefix.extend_from_slice(&0u16.to_le_bytes());
296 prefix.extend_from_slice(&32u16.to_le_bytes());
297 prefix.extend_from_slice(if codec == Codec::Vp8 {
298 b"VP80"
299 } else {
300 b"VP90"
301 });
302 prefix.extend_from_slice(&size.0.to_le_bytes());
303 prefix.extend_from_slice(&size.1.to_le_bytes());
304 prefix.extend_from_slice(&fps.num.to_le_bytes());
305 prefix.extend_from_slice(&fps.den.to_le_bytes());
306 prefix.extend_from_slice(&0u32.to_le_bytes());
307 prefix.extend_from_slice(&0u32.to_le_bytes());
308 }
309 prefix.extend_from_slice(&(payload.len() as u32).to_le_bytes());
310 prefix.extend_from_slice(&index.to_le_bytes());
311 prefix
312 }
313 Codec::Av1 if payload.first().is_none_or(|&b| (b >> 3) & 0x0f != 2) => vec![0x12, 0x00],
314 _ => Vec::new(),
315 }
316}
317
318fn write_all_frame<W: Write>(stream: &mut W, buf: &[u8], stop: &AtomicBool) -> std::io::Result<()> {
322 let mut written = 0usize;
323 while written < buf.len() {
324 if stop.load(Ordering::Relaxed) {
325 return Err(std::io::Error::other("writer stopped (client dropped)"));
326 }
327 match stream.write(&buf[written..]) {
328 Ok(0) => {
329 return Err(std::io::Error::new(
330 ErrorKind::WriteZero,
331 "failed to write whole frame",
332 ));
333 }
334 Ok(n) => written += n,
335 Err(ref e) if e.kind() == ErrorKind::TimedOut => {}
336 Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
337 thread::sleep(Duration::from_millis(5));
338 }
339 Err(ref e) if e.kind() == ErrorKind::Interrupted => {}
340 Err(e) => return Err(e),
341 }
342 }
343 Ok(())
344}
345
346#[cfg(test)]
347mod prefix_tests {
348 use super::*;
349
350 #[test]
354 fn prefixes_follow_the_codec() {
355 assert!(
356 stream_prefix(
357 Codec::H264,
358 &[0, 0, 1, 0x65],
359 0,
360 (1280, 720),
361 FrameRate::of(60.0)
362 )
363 .is_empty()
364 );
365 assert!(
366 stream_prefix(
367 Codec::H265,
368 &[0, 0, 1, 0x26, 1],
369 3,
370 (1280, 720),
371 FrameRate::of(60.0)
372 )
373 .is_empty()
374 );
375
376 let first = stream_prefix(
377 Codec::Vp9,
378 &[0x82, 0x49, 0x83],
379 0,
380 (1280, 720),
381 FrameRate::of(60.0),
382 );
383 assert_eq!(first.len(), 44);
384 assert_eq!(&first[..4], b"DKIF");
385 assert_eq!(&first[8..12], b"VP90");
386 assert_eq!(u16::from_le_bytes([first[12], first[13]]), 1280);
387 assert_eq!(u16::from_le_bytes([first[14], first[15]]), 720);
388 assert_eq!(
389 u32::from_le_bytes([first[16], first[17], first[18], first[19]]),
390 60
391 );
392 assert_eq!(
393 u32::from_le_bytes([first[20], first[21], first[22], first[23]]),
394 1
395 );
396 assert_eq!(
397 u32::from_le_bytes([first[32], first[33], first[34], first[35]]),
398 3
399 );
400 assert_eq!(u64::from_le_bytes(first[36..44].try_into().unwrap()), 0);
401 let later = stream_prefix(Codec::Vp8, &[0u8; 100], 7, (1280, 720), FrameRate::of(60.0));
402 assert_eq!(later.len(), 12);
403 assert_eq!(u32::from_le_bytes(later[..4].try_into().unwrap()), 100);
404 assert_eq!(u64::from_le_bytes(later[4..].try_into().unwrap()), 7);
405 assert_eq!(
406 &stream_prefix(Codec::Vp8, &[], 0, (64, 64), FrameRate::of(30.0))[8..12],
407 b"VP80"
408 );
409
410 assert!(
411 stream_prefix(
412 Codec::Av1,
413 &[0x12, 0x00, 0x32, 0x01, 0x10],
414 0,
415 (64, 64),
416 FrameRate::of(30.0)
417 )
418 .is_empty()
419 );
420 assert_eq!(
421 stream_prefix(
422 Codec::Av1,
423 &[0x32, 0x01, 0x10],
424 0,
425 (64, 64),
426 FrameRate::of(30.0)
427 ),
428 vec![0x12, 0x00]
429 );
430 assert_eq!(
431 stream_prefix(Codec::Av1, &[], 0, (64, 64), FrameRate::of(30.0)),
432 vec![0x12, 0x00]
433 );
434 let ntsc = stream_prefix(
435 Codec::Vp8,
436 &[],
437 0,
438 (64, 64),
439 FrameRate::of(60000.0 / 1001.0),
440 );
441 assert_eq!(
442 u32::from_le_bytes(ntsc[16..20].try_into().unwrap()),
443 60000,
444 "59.94 fps is 1001/60000 s a frame"
445 );
446 assert_eq!(u32::from_le_bytes(ntsc[20..24].try_into().unwrap()), 1001);
447 }
448}
449
450#[cfg(test)]
451mod cost_tests {
452 use super::*;
459 use std::io::Read;
460 use std::os::unix::net::UnixStream;
461 use std::time::Instant;
462
463 fn frame(len: usize) -> EncodedStripe {
464 let mut data = vec![0u8; len];
465 data[0] = WIRE_VIDEO;
466 EncodedStripe {
467 data: Arc::new(data),
468 codec: Codec::H264,
469 stripe_y_start: 0,
470 stripe_height: 720,
471 frame_id: 0,
472 timing: Default::default(),
473 reference: Default::default(),
474 }
475 }
476
477 fn feed_timed(sink: &RecordingSink, n: usize, len: usize) -> Vec<f64> {
479 let f = frame(len);
480 let mut us: Vec<f64> = (0..n)
481 .map(|_| {
482 let t = Instant::now();
483 sink.write_frame(std::slice::from_ref(&f), 1280, 720);
484 let elapsed = t.elapsed().as_secs_f64() * 1e6;
485 thread::sleep(Duration::from_micros(200));
486 elapsed
487 })
488 .collect();
489 us.sort_by(f64::total_cmp);
490 us
491 }
492
493 #[test]
496 fn stalled_recorder_isolation_cost() {
497 let path = format!("/tmp/pf-sink-cost-{}.sock", std::process::id());
498 let sink = RecordingSink::try_bind(&path, 60.0).expect("bind");
499
500 let idle = feed_timed(&sink, 500, 100_000);
502
503 let mut healthy = UnixStream::connect(&path).expect("connect");
505 thread::sleep(Duration::from_millis(200));
506 let drain = thread::spawn(move || {
507 let mut buf = vec![0u8; 1 << 20];
508 while healthy.read(&mut buf).map(|n| n > 0).unwrap_or(false) {}
509 });
510 let healthy = feed_timed(&sink, 500, 100_000);
511
512 let reader = UnixStream::connect(&path).expect("connect");
515 thread::sleep(Duration::from_millis(200));
516 let stalled = feed_timed(&sink, 500, 100_000);
517 drop(reader);
518
519 let median = |us: &[f64]| us[us.len() / 2];
520 let p99 = |us: &[f64]| us[us.len() * 99 / 100];
521 for (phase, us) in [
522 ("idle ", &idle),
523 ("healthy", &healthy),
524 ("stalled", &stalled),
525 ] {
526 println!(
527 "[sink-cost] {phase} median {:.3}us p99 {:.3}us",
528 median(us),
529 p99(us)
530 );
531 }
532 drop(sink);
533 let _ = drain.join();
534
535 assert!(
536 median(&idle) < 5.0,
537 "idle feed should be sub-5us, was {:.3}us",
538 median(&idle)
539 );
540 assert!(
541 median(&healthy) < 100.0,
542 "healthy feed should be tens of us, was {:.3}us",
543 median(&healthy)
544 );
545 assert!(
546 p99(&stalled) < 10_000.0,
547 "a stalled recorder must never block the tap >10ms, was {:.3}us",
548 p99(&stalled)
549 );
550 }
551}