use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use rff::core::{AudioFrame, CodecId, Dictionary, Frame, MediaType, SampleFormat};
use rff::format::Stream;
use rff::transcode::{InputSpec, OutputSpec, StreamCodec, TranscodeSpec};
use rff::Engine;
fn tmp(name: &str, ext: &str) -> PathBuf {
std::env::temp_dir().join(format!("rff_edit_{}_{name}.{ext}", std::process::id()))
}
fn write_wav(engine: &Engine, path: &Path, pcm: &[u8], sr: u32, ch: u16) {
let af = AudioFrame {
sample_rate: sr,
channels: ch,
format: SampleFormat::S16,
planes: vec![pcm.to_vec()],
samples: pcm.len() / (2 * ch as usize),
pts: Some(0),
};
let mut enc = engine.codecs.find_encoder(CodecId::Pcm).unwrap();
enc.send_frame(&Frame::Audio(af)).unwrap();
enc.flush();
let packet = enc.receive_packet().unwrap();
let mut mux = engine
.formats
.open_muxer("wav", Box::new(fs::File::create(path).unwrap()))
.unwrap();
let mut s = Stream::new(0, CodecId::Pcm);
s.sample_rate = sr;
s.channels = ch;
s.sample_format = Some(SampleFormat::S16);
mux.write_header(&[s]).unwrap();
mux.write_packet(&packet).unwrap();
mux.write_trailer().unwrap();
}
fn read_wav(engine: &Engine, path: &Path) -> (Stream, Vec<u8>) {
let mut dem = engine
.formats
.open_demuxer("wav", Box::new(fs::File::open(path).unwrap()))
.unwrap();
let streams = dem.read_header().unwrap();
let mut data = Vec::new();
while let Ok(p) = dem.read_packet() {
data.extend_from_slice(&p.data);
}
(streams[0].clone(), data)
}
fn write_y4m(path: &Path, frames: usize) {
let mut f = fs::File::create(path).unwrap();
write!(f, "YUV4MPEG2 W16 H16 F25:1 Ip A1:1 C420mpeg2\n").unwrap();
for i in 0..frames {
write!(f, "FRAME\n").unwrap();
f.write_all(&vec![(16 + i) as u8; 16 * 16]).unwrap(); f.write_all(&[128u8; 8 * 8]).unwrap(); f.write_all(&[128u8; 8 * 8]).unwrap(); }
}
fn audio_out(codec: CodecId) -> Option<StreamCodec> {
Some(StreamCodec {
codec,
options: Dictionary::new(),
sample_format: None,
})
}
fn s16_ramp(n: usize) -> Vec<u8> {
(0..n)
.flat_map(|k| ((k / 8) as i16).to_le_bytes())
.collect()
}
#[test]
fn ss_t_trims_audio_sample_accurately() {
let engine = Engine::new();
let in_path = tmp("trim_in", "wav");
let out_path = tmp("trim_out", "wav");
write_wav(&engine, &in_path, &s16_ramp(16_000), 8_000, 1);
let spec = TranscodeSpec {
inputs: vec![InputSpec {
path: in_path.clone(),
format: None,
}],
outputs: vec![OutputSpec {
path: out_path.clone(),
audio_codec: audio_out(CodecId::Pcm),
overwrite: true,
trim_start: Some(0.5),
trim_end: Some(1.5),
..Default::default()
}],
};
rff::transcode::run(&engine, &spec).expect("trim transcode");
let (s, data) = read_wav(&engine, &out_path);
assert_eq!(s.sample_rate, 8_000);
assert_eq!(data.len() / 2, 8_000);
let first = i16::from_le_bytes([data[0], data[1]]);
assert_eq!(first, (4000 / 8) as i16);
for p in [in_path, out_path] {
let _ = fs::remove_file(p);
}
}
#[test]
fn ac_downmixes_stereo_to_mono() {
let engine = Engine::new();
let in_path = tmp("ac_in", "wav");
let out_path = tmp("ac_out", "wav");
let pcm: Vec<u8> = (0..1000)
.flat_map(|_| {
let mut b = 1000i16.to_le_bytes().to_vec();
b.extend_from_slice(&3000i16.to_le_bytes());
b
})
.collect();
write_wav(&engine, &in_path, &pcm, 8_000, 2);
let spec = TranscodeSpec {
inputs: vec![InputSpec {
path: in_path.clone(),
format: None,
}],
outputs: vec![OutputSpec {
path: out_path.clone(),
audio_codec: Some(StreamCodec {
codec: CodecId::Pcm,
options: Dictionary::new(),
sample_format: Some(SampleFormat::S16),
}),
overwrite: true,
audio_channels: Some(1),
..Default::default()
}],
};
rff::transcode::run(&engine, &spec).expect("-ac transcode");
let (s, data) = read_wav(&engine, &out_path);
assert_eq!(s.channels, 1);
assert_eq!(data.len() / 2, 1000);
let first = i16::from_le_bytes([data[0], data[1]]);
assert_eq!(first, 2000);
for p in [in_path, out_path] {
let _ = fs::remove_file(p);
}
}
#[test]
fn af_volume_scales_samples() {
let engine = Engine::new();
let in_path = tmp("vol_in", "wav");
let out_path = tmp("vol_out", "wav");
let pcm: Vec<u8> = (0..1000).flat_map(|_| 1000i16.to_le_bytes()).collect();
write_wav(&engine, &in_path, &pcm, 8_000, 1);
let spec = TranscodeSpec {
inputs: vec![InputSpec {
path: in_path.clone(),
format: None,
}],
outputs: vec![OutputSpec {
path: out_path.clone(),
audio_codec: Some(StreamCodec {
codec: CodecId::Pcm,
options: Dictionary::new(),
sample_format: Some(SampleFormat::S16),
}),
overwrite: true,
audio_filters: Some("volume=2.0".into()),
..Default::default()
}],
};
rff::transcode::run(&engine, &spec).expect("-af transcode");
let (_, data) = read_wav(&engine, &out_path);
let first = i16::from_le_bytes([data[0], data[1]]);
assert_eq!(first, 2000);
for p in [in_path, out_path] {
let _ = fs::remove_file(p);
}
}
#[test]
fn r_upsamples_frame_rate_by_duplication() {
let engine = Engine::new();
let in_path = tmp("fps_in", "y4m");
let out_path = tmp("fps_out", "y4m");
write_y4m(&in_path, 10);
let spec = TranscodeSpec {
inputs: vec![InputSpec {
path: in_path.clone(),
format: None,
}],
outputs: vec![OutputSpec {
path: out_path.clone(),
overwrite: true,
frame_rate: Some((50, 1)),
..Default::default()
}],
};
rff::transcode::run(&engine, &spec).expect("-r transcode");
let head = fs::read(&out_path).unwrap();
let header = String::from_utf8_lossy(&head[..64.min(head.len())]).to_string();
assert!(header.contains("F50:1"), "header: {header}");
let frames = head.windows(5).filter(|w| w == b"FRAME").count();
assert!(
(19..=21).contains(&frames),
"expected ~20 output frames, got {frames}"
);
for p in [in_path, out_path] {
let _ = fs::remove_file(p);
}
}
#[test]
fn vp9_encodes_into_webm_and_demuxes_back() {
let engine = Engine::new();
let in_path = tmp("webm_in", "y4m");
let out_path = tmp("webm_out", "webm");
write_y4m(&in_path, 5);
let mut opts = Dictionary::new();
opts.set("speed", "4"); let spec = TranscodeSpec {
inputs: vec![InputSpec {
path: in_path.clone(),
format: None,
}],
outputs: vec![OutputSpec {
path: out_path.clone(),
video_codec: Some(StreamCodec {
codec: CodecId::Vp9,
options: opts,
sample_format: None,
}),
overwrite: true,
..Default::default()
}],
};
let report = rff::transcode::run(&engine, &spec).expect("webm encode");
assert!(report.packets_written >= 5);
let mut dem = engine
.formats
.open_demuxer("matroska", Box::new(fs::File::open(&out_path).unwrap()))
.unwrap();
let streams = dem.read_header().unwrap();
assert_eq!(streams.len(), 1);
assert_eq!(streams[0].codec_id, CodecId::Vp9);
assert_eq!(streams[0].media_type, MediaType::Video);
assert_eq!((streams[0].width, streams[0].height), (16, 16));
let p0 = dem.read_packet().unwrap();
let p1 = dem.read_packet().unwrap();
assert!(p0.flags.keyframe);
assert_eq!(p0.pts, Some(0));
assert_eq!(p1.pts, Some(40));
let bytes = fs::read(&out_path).unwrap();
assert_eq!(&bytes[..4], &[0x1A, 0x45, 0xDF, 0xA3]);
let head = &bytes[..64.min(bytes.len())];
assert!(
head.windows(4).any(|w| w == b"webm"),
"missing webm doctype"
);
for p in [in_path, out_path] {
let _ = fs::remove_file(p);
}
}