1use std::ffi::OsStr;
6use std::io::{self, Read as _};
7use std::process::{Child, Command, Stdio};
8use std::sync::mpsc::{Receiver, TryRecvError, sync_channel};
9use std::thread;
10use std::time::Instant;
11
12#[derive(Debug)]
15pub struct VideoStream {
16 frames: Receiver<Vec<u8>>,
17 decoder: Child,
18 frames_per_second: u32,
19 started: Instant,
20 shown: u32,
21 ended: bool,
22}
23
24impl VideoStream {
25 pub fn spawn(
28 mut command: Command,
29 (width, height): (u32, u32),
30 frames_per_second: u32,
31 ) -> io::Result<Self> {
32 let mut decoder = command
33 .stdin(Stdio::null())
34 .stdout(Stdio::piped())
35 .stderr(Stdio::null())
36 .spawn()?;
37 let mut output = decoder
38 .stdout
39 .take()
40 .ok_or_else(|| io::Error::other("the decoder gave no output pipe"))?;
41 let (send, frames) = sync_channel(frames_per_second.max(4) as usize / 4);
42 let frame_bytes = width as usize * height as usize * 4;
43 thread::spawn(move || {
44 loop {
45 let mut frame = vec![0; frame_bytes];
46 if output.read_exact(&mut frame).is_err() || send.send(frame).is_err() {
47 break;
48 }
49 }
50 });
51 Ok(Self {
52 frames,
53 decoder,
54 frames_per_second: frames_per_second.max(1),
55 started: Instant::now(),
56 shown: 0,
57 ended: false,
58 })
59 }
60
61 pub fn ffmpeg<S: AsRef<OsStr>>(
65 input: &[S],
66 size: (u32, u32),
67 frames_per_second: u32,
68 ) -> io::Result<Self> {
69 let mut command = Command::new("ffmpeg");
70 command
71 .args(["-hide_banner", "-loglevel", "error", "-nostdin"])
72 .args(input)
73 .args(["-an", "-vf", &fit_filter(size, frames_per_second)])
74 .args(["-f", "rawvideo", "-pix_fmt", "rgba", "-"]);
75 Self::spawn(command, size, frames_per_second)
76 }
77
78 pub fn latest(&mut self) -> Option<Vec<u8>> {
83 let passed = self.started.elapsed().as_millis() * u128::from(self.frames_per_second) / 1000;
84 let due = u32::try_from(passed).unwrap_or(u32::MAX).saturating_add(1);
85 let mut newest = None;
86 while self.shown < due {
87 match self.frames.try_recv() {
88 Ok(frame) => {
89 newest = Some(frame);
90 self.shown += 1;
91 }
92 Err(TryRecvError::Empty) => break,
93 Err(TryRecvError::Disconnected) => {
94 self.ended = true;
95 break;
96 }
97 }
98 }
99 newest
100 }
101
102 #[must_use]
104 pub fn shown(&self) -> u32 {
105 self.shown
106 }
107
108 #[must_use]
110 pub fn ended(&self) -> bool {
111 self.ended
112 }
113}
114
115impl Drop for VideoStream {
116 fn drop(&mut self) {
117 let _ = self.decoder.kill();
118 let _ = self.decoder.wait();
119 }
120}
121
122fn fit_filter((width, height): (u32, u32), frames_per_second: u32) -> String {
125 format!(
126 "fps={frames_per_second},scale={width}:{height}:force_original_aspect_ratio=decrease,pad={width}:{height}:-1:-1:black"
127 )
128}
129
130#[cfg(test)]
131mod tests {
132 use std::process::Command;
133 use std::time::{Duration, Instant};
134
135 use super::{VideoStream, fit_filter};
136
137 #[test]
138 fn the_filter_letterboxes_to_the_frame_at_the_frame_rate() {
139 assert_eq!(
140 fit_filter((320, 180), 30),
141 "fps=30,scale=320:180:force_original_aspect_ratio=decrease,pad=320:180:-1:-1:black"
142 );
143 }
144
145 #[test]
146 fn a_stream_hands_out_the_newest_due_frame_then_ends() {
147 let mut command = Command::new("sh");
148 command.args(["-c", "printf 'aaaabbbbccccdddd'"]);
149 let mut stream = VideoStream::spawn(command, (2, 1), 1000).expect("sh runs");
150 let deadline = Instant::now() + Duration::from_secs(5);
151 let mut last = None;
152 while !stream.ended() && Instant::now() < deadline {
153 if let Some(frame) = stream.latest() {
154 last = Some(frame);
155 }
156 std::thread::sleep(Duration::from_millis(5));
157 }
158 assert!(stream.ended(), "the decoder exited");
159 assert_eq!(stream.shown(), 2);
160 assert_eq!(last.as_deref(), Some(&b"ccccdddd"[..]));
161 }
162}