use std::ffi::OsStr;
use std::io::{self, Read as _};
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{Receiver, TryRecvError, sync_channel};
use std::thread;
use std::time::Instant;
#[derive(Debug)]
pub struct VideoStream {
frames: Receiver<Vec<u8>>,
decoder: Child,
frames_per_second: u32,
started: Instant,
shown: u32,
ended: bool,
}
impl VideoStream {
pub fn spawn(
mut command: Command,
(width, height): (u32, u32),
frames_per_second: u32,
) -> io::Result<Self> {
let mut decoder = command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
let mut output = decoder
.stdout
.take()
.ok_or_else(|| io::Error::other("the decoder gave no output pipe"))?;
let (send, frames) = sync_channel(frames_per_second.max(4) as usize / 4);
let frame_bytes = width as usize * height as usize * 4;
thread::spawn(move || {
loop {
let mut frame = vec![0; frame_bytes];
if output.read_exact(&mut frame).is_err() || send.send(frame).is_err() {
break;
}
}
});
Ok(Self {
frames,
decoder,
frames_per_second: frames_per_second.max(1),
started: Instant::now(),
shown: 0,
ended: false,
})
}
pub fn ffmpeg<S: AsRef<OsStr>>(
input: &[S],
size: (u32, u32),
frames_per_second: u32,
) -> io::Result<Self> {
let mut command = Command::new("ffmpeg");
command
.args(["-hide_banner", "-loglevel", "error", "-nostdin"])
.args(input)
.args(["-an", "-vf", &fit_filter(size, frames_per_second)])
.args(["-f", "rawvideo", "-pix_fmt", "rgba", "-"]);
Self::spawn(command, size, frames_per_second)
}
pub fn latest(&mut self) -> Option<Vec<u8>> {
let passed = self.started.elapsed().as_millis() * u128::from(self.frames_per_second) / 1000;
let due = u32::try_from(passed).unwrap_or(u32::MAX).saturating_add(1);
let mut newest = None;
while self.shown < due {
match self.frames.try_recv() {
Ok(frame) => {
newest = Some(frame);
self.shown += 1;
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
self.ended = true;
break;
}
}
}
newest
}
#[must_use]
pub fn shown(&self) -> u32 {
self.shown
}
#[must_use]
pub fn ended(&self) -> bool {
self.ended
}
}
impl Drop for VideoStream {
fn drop(&mut self) {
let _ = self.decoder.kill();
let _ = self.decoder.wait();
}
}
fn fit_filter((width, height): (u32, u32), frames_per_second: u32) -> String {
format!(
"fps={frames_per_second},scale={width}:{height}:force_original_aspect_ratio=decrease,pad={width}:{height}:-1:-1:black"
)
}
#[cfg(test)]
mod tests {
use std::process::Command;
use std::time::{Duration, Instant};
use super::{VideoStream, fit_filter};
#[test]
fn the_filter_letterboxes_to_the_frame_at_the_frame_rate() {
assert_eq!(
fit_filter((320, 180), 30),
"fps=30,scale=320:180:force_original_aspect_ratio=decrease,pad=320:180:-1:-1:black"
);
}
#[test]
fn a_stream_hands_out_the_newest_due_frame_then_ends() {
let mut command = Command::new("sh");
command.args(["-c", "printf 'aaaabbbbccccdddd'"]);
let mut stream = VideoStream::spawn(command, (2, 1), 1000).expect("sh runs");
let deadline = Instant::now() + Duration::from_secs(5);
let mut last = None;
while !stream.ended() && Instant::now() < deadline {
if let Some(frame) = stream.latest() {
last = Some(frame);
}
std::thread::sleep(Duration::from_millis(5));
}
assert!(stream.ended(), "the decoder exited");
assert_eq!(stream.shown(), 2);
assert_eq!(last.as_deref(), Some(&b"ccccdddd"[..]));
}
}