#![allow(clippy::unwrap_used, clippy::expect_used, clippy::cast_precision_loss)]
mod fixtures;
use avio::{Clip, EncoderConfig, Timeline};
use ff_decode::VideoDecoder;
use ff_encode::{AudioCodec, BitrateMode, VideoCodec};
use fixtures::{FileGuard, make_source_file, test_output_path};
#[test]
fn composition_does_not_stretch_non_30fps_video() {
let src = test_output_path("compfps_src_24.mp4");
let _gs = FileGuard::new(src.clone());
let frame_count = 48usize; if make_source_file(&src, 320, 240, 24.0, frame_count, 76, 84, 255).is_none() {
return; }
let out = test_output_path("compfps_out_24.mp4");
let _go = FileGuard::new(out.clone());
let clip = Clip::new(&src);
let timeline = match Timeline::builder()
.video_track(vec![clip.clone()])
.audio_track(vec![clip])
.build()
{
Ok(t) => t,
Err(e) => {
println!("Skipping: Timeline::builder().build() failed: {e}");
return;
}
};
let cfg = EncoderConfig::builder()
.video_codec(VideoCodec::H264)
.audio_codec(AudioCodec::Aac)
.bitrate_mode(BitrateMode::Cbr(800_000))
.build();
if timeline.render(&out, cfg).is_err() {
return; }
let mut last = 0.0f64;
let Ok(mut d) = VideoDecoder::open(&out).build() else {
println!("Skipping: cannot open exported file for decode");
return;
};
while let Ok(Some(f)) = d.decode_one() {
last = f.timestamp().as_duration().as_secs_f64();
}
let expected = (frame_count - 1) as f64 / 24.0; assert!(
(last - expected).abs() < 0.2,
"exported 24fps video was stretched: last_pts={last:.3}s expected~{expected:.3}s \
— the composition canvas rate must follow the timeline frame_rate"
);
}