#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::io::{Cursor, Seek, SeekFrom, Write};
use std::sync::{Arc, Mutex};
use ff_encode::{BitrateMode, VideoCodec, VideoEncoder};
use ff_format::VideoFrame;
const W: u32 = 64;
const H: u32 = 64;
const FRAMES: usize = 10;
#[derive(Clone)]
struct SharedSink(Arc<Mutex<Cursor<Vec<u8>>>>);
impl SharedSink {
fn new() -> Self {
Self(Arc::new(Mutex::new(Cursor::new(Vec::new()))))
}
fn bytes(&self) -> Vec<u8> {
self.0
.lock()
.map_or_else(|_| Vec::new(), |c| c.get_ref().clone())
}
}
impl Write for SharedSink {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let mut guard = self
.0
.lock()
.map_err(|_| std::io::Error::other("sink poisoned"))?;
guard.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Seek for SharedSink {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let mut guard = self
.0
.lock()
.map_err(|_| std::io::Error::other("sink poisoned"))?;
guard.seek(pos)
}
}
fn frame() -> Option<VideoFrame> {
VideoFrame::from_rgba(W, H, vec![128u8; (W * H * 4) as usize]).ok()
}
fn encode_into(sink: SharedSink) -> Option<()> {
let mut encoder = VideoEncoder::create("out.mp4")
.video(W, H, 30.0)
.video_codec(VideoCodec::H264)
.bitrate_mode(BitrateMode::Cbr(400_000))
.output_sink(sink)
.build()
.ok()?;
let f = frame()?;
for _ in 0..FRAMES {
encoder.push_video(&f).ok()?;
}
encoder.finish().ok()
}
fn encode_to_file() -> Option<(u64, usize)> {
let dir = std::env::temp_dir().join("avio-custom-io-tests");
std::fs::create_dir_all(&dir).ok()?;
let path = dir.join("control.mp4");
let _ = std::fs::remove_file(&path);
let mut encoder = VideoEncoder::create(&path)
.video(W, H, 30.0)
.video_codec(VideoCodec::H264)
.bitrate_mode(BitrateMode::Cbr(400_000))
.build()
.ok()?;
let f = frame()?;
for _ in 0..FRAMES {
encoder.push_video(&f).ok()?;
}
encoder.finish().ok()?;
let len = std::fs::metadata(&path).ok()?.len();
let mut decoder = ff_decode::VideoDecoder::open(&path).build().ok()?;
let mut frames = 0usize;
while let Ok(Some(_)) = decoder.decode_one() {
frames += 1;
}
let _ = std::fs::remove_file(&path);
Some((len, frames))
}
#[test]
fn encoding_into_an_in_memory_sink_should_produce_a_decodable_stream() {
let sink = SharedSink::new();
if encode_into(sink.clone()).is_none() {
return; }
let Some((file_len, file_frames)) = encode_to_file() else {
return;
};
let bytes = sink.bytes();
let mut decoder = ff_decode::VideoDecoder::from_reader(Cursor::new(bytes.clone()))
.build()
.expect("the sink's bytes must be a decodable stream");
let mut decoded = 0usize;
while let Ok(Some(_)) = decoder.decode_one() {
decoded += 1;
}
println!(
"sink: {} bytes / {decoded} frames | file: {file_len} bytes / {file_frames} frames",
bytes.len()
);
assert!(file_frames > 0, "the control must decode something");
assert_eq!(
bytes.len() as u64,
file_len,
"the sink must receive the same output the file route writes"
);
assert_eq!(
decoded, file_frames,
"the sink's stream must decode to the same frames as the file's"
);
}
#[test]
fn a_sink_should_receive_the_whole_stream_not_just_its_head() {
let sink = SharedSink::new();
if encode_into(sink.clone()).is_none() {
return;
}
let bytes = sink.bytes();
assert!(
bytes.len() > 1024,
"a 10-frame stream should be more than a header, got {} bytes",
bytes.len()
);
assert!(
bytes.windows(4).any(|w| w == b"mdat"),
"the muxed payload must have reached the sink"
);
}
#[test]
fn a_sink_and_faststart_should_be_rejected_at_build_time() {
let built = VideoEncoder::create("out.mp4")
.video(W, H, 30.0)
.video_codec(VideoCodec::H264)
.faststart()
.output_sink(SharedSink::new())
.build();
assert!(
built.is_err(),
"faststart with a caller-supplied sink must be rejected"
);
}
#[test]
fn a_sink_and_a_self_managing_muxer_should_be_rejected() {
let dir = std::env::temp_dir().join("avio-custom-io-tests");
let _ = std::fs::create_dir_all(&dir);
let pattern = dir.join("frame_%03d.mp4");
let control = VideoEncoder::create(&pattern)
.video(W, H, 30.0)
.video_codec(VideoCodec::H264)
.build();
if control.is_err() {
return; }
drop(control);
let built = VideoEncoder::create(&pattern)
.video(W, H, 30.0)
.video_codec(VideoCodec::H264)
.output_sink(SharedSink::new())
.build();
assert!(
built.is_err(),
"a self-managing muxer with a caller-supplied sink must be rejected"
);
}
#[test]
fn a_sink_and_two_pass_should_be_rejected_at_build_time() {
let built = VideoEncoder::create("out.mp4")
.video(W, H, 30.0)
.video_codec(VideoCodec::H264)
.two_pass()
.output_sink(SharedSink::new())
.build();
assert!(
built.is_err(),
"two-pass with a caller-supplied sink must be rejected"
);
}