99 lines
4 KiB
Rust
99 lines
4 KiB
Rust
//! Probe: the same frames encoded twice, how alike are the files (edit
|
|
//! exports should be byte-reproducible). Ignored: it runs the hardware
|
|
//! encoder. Found (2026-09-30, M-series): H.264 coded frames come out
|
|
//! identical run to run, with or without an interleaved audio track, even
|
|
//! with four probes at once; the files still differ in the movie header's
|
|
//! creation and modification times (mvhd, tkhd, mdhd) and in two bytes of
|
|
//! Apple's user-data SEI on the first frame (a per-session value). H.265
|
|
//! differs throughout. And an edit export under heavy load (four renders at
|
|
//! once) once diverged from frame 30 on: the writer's inputs run with
|
|
//! expectsMediaDataInRealTime (the synchronous push API needs it), and a
|
|
//! real-time VideoToolbox session may decide rate control by timing.
|
|
|
|
#![cfg(target_os = "macos")]
|
|
|
|
use makepad_video::{PcmAudioTrackOptions, VideoFileCodec, VideoFileEncoder, VideoFileEncoderOptions};
|
|
|
|
const W: u32 = 640;
|
|
const H: u32 = 360;
|
|
|
|
fn frame(k: usize) -> Vec<u8> {
|
|
let mut out = vec![0u8; W as usize * H as usize * 3];
|
|
for y in 0..H as usize {
|
|
for x in 0..W as usize {
|
|
let i = (y * W as usize + x) * 3;
|
|
let v = ((x as f32 * 0.05 + k as f32 * 0.3).sin() * (y as f32 * 0.07 - k as f32 * 0.2).cos() * 127.0 + 128.0) as u8;
|
|
out[i] = v;
|
|
out[i + 1] = ((x + k * 3) % 256) as u8;
|
|
out[i + 2] = ((y * 2 + k) % 256) as u8;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// With `audio`, a second of sound leads the pictures, as an edit export
|
|
/// interleaves them.
|
|
fn encode_with(codec: VideoFileCodec, tag: &str, audio: bool) -> Vec<u8> {
|
|
let dir = std::env::temp_dir().join("makepad-video-repeatable");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let path = dir.join(format!("{tag}.mp4"));
|
|
let mut e = VideoFileEncoder::new(
|
|
path.to_str().unwrap(),
|
|
VideoFileEncoderOptions {
|
|
codec,
|
|
width: W,
|
|
height: H,
|
|
fps_num: 30,
|
|
fps_den: 1,
|
|
video_bitrate_bps: 2_000_000,
|
|
audio: audio.then_some(PcmAudioTrackOptions { sample_rate: 48_000, channels: 2, aac_bitrate_bps: 192_000 }),
|
|
keyframe_only: false,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
let pcm = |from: usize, to: usize| -> Vec<i16> {
|
|
(from..to).flat_map(|i| { let v = ((i as f32 * 0.0575).sin() * 9000.0) as i16; [v, v] }).collect()
|
|
};
|
|
for k in 0..90usize {
|
|
e.push_frame_rgb8(&frame(k), Some(k as i64 * 10_000_000 / 30)).unwrap();
|
|
if audio {
|
|
let (a, b) = if k == 0 { (0, 48_000) } else { (48_000 + (k - 1) * 1600, 48_000 + k * 1600) };
|
|
e.push_audio_i16(&pcm(a, b)).unwrap();
|
|
}
|
|
}
|
|
e.finish().unwrap();
|
|
std::fs::read(&path).unwrap()
|
|
}
|
|
|
|
/// Top-level boxes and where two files first differ.
|
|
fn first_difference(a: &[u8], b: &[u8]) -> Option<(usize, String)> {
|
|
let at = a.iter().zip(b).position(|(x, y)| x != y).or((a.len() != b.len()).then(|| a.len().min(b.len())))?;
|
|
let mut off = 0usize;
|
|
let mut path = String::new();
|
|
while off + 8 <= a.len() {
|
|
let size = u32::from_be_bytes(a[off..off + 4].try_into().unwrap()) as usize;
|
|
let name = String::from_utf8_lossy(&a[off + 4..off + 8]).to_string();
|
|
if size < 8 {
|
|
break;
|
|
}
|
|
if at < off + size {
|
|
path = format!("{name} at {off}+{}", at - off);
|
|
break;
|
|
}
|
|
off += size;
|
|
}
|
|
Some((at, path))
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn the_same_frames_encode_to_the_same_file() {
|
|
for (codec, audio) in [(VideoFileCodec::H264, false), (VideoFileCodec::H264, true), (VideoFileCodec::H265, false)] {
|
|
let first = encode_with(codec, &format!("{codec:?}-{audio}-0"), audio);
|
|
for run in 1..4 {
|
|
let again = encode_with(codec, &format!("{codec:?}-{audio}-{run}"), audio);
|
|
println!("{codec:?} audio {audio} run {run}: {} vs {} bytes, first difference {:?}", first.len(), again.len(), first_difference(&first, &again));
|
|
}
|
|
}
|
|
}
|