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