Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/tests/matroska_frames.rs

9.3 KiB, 24 runs

created by r1870400018:21780, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Does the cluster reader hand back the same frames a player demuxes?
2//!
3//! [`matroska_corpus`](../matroska_corpus.rs) asks whether the *description* of
4//! a film is right. This asks the harder question: whether the frames lifted out
5//! of the clusters are the ones that are actually there. It matters more,
6//! because the point of reading clusters at all is repackaging, and a
7//! repackager that drops a frame, mistimes one, or glues a laced pair together
8//! produces a file that plays -- badly, or for a while, or with the sound adrift
9//! -- rather than one that fails.
10//!
11//! **The oracle is `ffprobe -show_packets`**, which demuxes the same file with a
12//! wholly separate implementation and reports every packet's size, presentation
13//! time and whether decoding may begin at it. Four things are compared per
14//! frame, in order: the count, the size in bytes, the time, and the keyframe
15//! flag. A reader that found the clusters but mis-split the blocks agrees on
16//! none of them.
17//!
18//! Point `MKV_CORPUS` at a directory of films. `MKV_FRAMES` caps how many frames
19//! of each are compared, because a two-hour film holds two hundred thousand and
20//! the disease this catches shows itself in the first thousand.
21//!
22//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
23//! Anthropic Claude
24
25use oxedyne_fe2o3_core::prelude::*;
26use oxedyne_fe2o3_graphics::matroska::{Clusters, Matroska, TrackKind};
27
28use std::{
29 env,
30 fs::{self, File},
31 io::Read,
32 path::{Path, PathBuf},
33 process::Command,
34};
35
36const HEAD: usize = 1024 * 1024; // read before the clusters, to find the tracks
37
38// The window the streaming reader is held to, in bytes. Deliberately small --
39// far smaller than a cluster, which is megabytes -- so that the test fails if
40// the reader ever needs a whole cluster in hand. A window this size passing is
41// the evidence that a four-gigabyte film can be repackaged without being held.
42const WINDOW: usize = 256 * 1024;
43
44const FRAMES: usize = 4000; // frames of each film compared unless MKV_FRAMES says otherwise
45const FILES: usize = 8; // films read unless MKV_FILES says otherwise
46
47/// One frame, as either implementation describes it.
48#[derive(Clone, Copy, Debug, PartialEq)]
49struct Packet {
50 size: usize,
51 time: i64,
52 key: bool,
53}
54
55#[test]
56fn frames_agree_with_the_player() -> Outcome<()> {
57 let dir = match env::var("MKV_CORPUS") {
58 Ok(d) => PathBuf::from(d),
59 Err(_) => {
60 println!("MKV_CORPUS is not set, so no film was read. This test \
61 compares real films against ffprobe and proves nothing without \
62 them.");
63 return Ok(());
64 },
65 };
66 if Command::new("ffprobe").arg("-version").output().is_err() {
67 println!("ffprobe is not installed, so there is no oracle to compare \
68 against and this test proves nothing.");
69 return Ok(());
70 }
71
72 let want = num_from_env("MKV_FRAMES", FRAMES);
73 let most = num_from_env("MKV_FILES", FILES);
74
75 let mut films = Vec::new();
76 res!(gather(&dir, &mut films));
77 films.sort();
78
79 let mut compared = 0usize;
80 let mut refused = 0usize;
81 let mut no_oracle = 0usize;
82 let mut frames = 0usize;
83
84 for film in films.iter() {
85 if compared >= most {
86 break;
87 }
88 // Both streams, and the sound is not an afterthought: **lacing lives in
89 // audio**. A film's picture is one frame a block and exercises none of
90 // the three lacing schemes, so a reader checked on the picture alone has
91 // its lacing arithmetic wholly unproven against anything real.
92 let mut both = 0usize;
93 for (kind, pick) in [(TrackKind::Video, "v:0"), (TrackKind::Audio, "a:0")] {
94 let ours = match read_ours(film, want, kind) {
95 Ok(Some(v)) => v,
96 // A film with no such track, or one this reader will not open,
97 // is counted rather than passed over in silence.
98 Ok(None) => continue,
99 Err(e) => {
100 println!("{}: {} refused by the reader: {}", film.display(), pick, e);
101 refused += 1;
102 continue;
103 },
104 };
105 let theirs = match read_theirs(film, want, pick) {
106 Some(v) => v,
107 None => { no_oracle += 1; continue },
108 };
109 if theirs.is_empty() {
110 no_oracle += 1;
111 continue;
112 }
113
114 if ours.len() != theirs.len() {
115 return Err(err!(
116 "{}: on {} the reader found {} frames and ffprobe found {}.",
117 film.display(), pick, ours.len(), theirs.len();
118 Invalid, Mismatch));
119 }
120 for i in 0..ours.len() {
121 if ours[i] != theirs[i] {
122 return Err(err!(
123 "{}: on {}, frame {} of {} differs. The reader says {:?} \
124 and ffprobe says {:?}.",
125 film.display(), pick, i, ours.len(), ours[i], theirs[i];
126 Invalid, Mismatch));
127 }
128 }
129 frames += ours.len();
130 both += 1;
131 println!("{}: {} {} frames agree", film.display(), ours.len(), pick);
132 }
133 if both == 0 {
134 refused += 1;
135 continue;
136 }
137 compared += 1;
138 }
139
140 // Every film is accounted for, so that a run which quietly compared nothing
141 // cannot read as a pass.
142 println!(
143 "{} films found; {} compared ({} frames), {} refused, {} without an oracle",
144 films.len(), compared, frames, refused, no_oracle);
145 if !films.is_empty() && compared == 0 {
146 return Err(err!(
147 "{} films were found and not one was compared, so this run proves \
148 nothing.", films.len();
149 Invalid, Mismatch));
150 }
151 Ok(())
152}
153
154/// Reads a film's frames through the streaming reader, holding only a window.
155///
156/// `None` where the film carries no picture track to compare.
157fn read_ours(path: &Path, want: usize, kind: TrackKind) -> Outcome<Option<Vec<Packet>>> {
158 let mut head = vec![0u8; HEAD];
159 let mut file = res!(File::open(path));
160 let n = res!(file.read(&mut head));
161 head.truncate(n);
162 let mkv = res!(Matroska::read(&head));
163 // The **first** track of the kind, in file order, because that is what
164 // ffprobe's `v:0` and `a:0` mean. Deliberately not `Matroska::video()`,
165 // which prefers a default-flagged stream and would compare two different
166 // streams on a film carrying an alternative take.
167 let track = match mkv.tracks().iter().find(|t| t.kind() == Some(kind)) {
168 Some(t) => {
169 if env::var("MKV_SAY_TRACKS").is_ok() {
170 println!(" track {} {:?} {} frame_ns={} rate={}",
171 t.number(), t.kind(), t.codec(), t.frame_nanos(), t.rate());
172 }
173 t.number()
174 },
175 None => return Ok(None),
176 };
177
178 let mut file = res!(File::open(path));
179 let mut cl = Clusters::new(&mkv);
180 let mut buf: Vec<u8> = Vec::new();
181 let mut out = Vec::new();
182 let mut done = false;
183
184 loop {
185 // Top the window up. Reading stops at the end of the file, which is not
186 // an error -- the reader is simply told nothing more is coming.
187 while buf.len() < WINDOW && !done {
188 let mut chunk = vec![0u8; WINDOW];
189 let got = res!(file.read(&mut chunk));
190 if got == 0 {
191 done = true;
192 break;
193 }
194 chunk.truncate(got);
195 buf.extend_from_slice(&chunk);
196 }
197 if buf.is_empty() {
198 break;
199 }
200
201 let mut full = false;
202 let fed = res!(cl.feed(&buf, &mut |frame| {
203 if frame.track == track && out.len() < want {
204 out.push(Packet {
205 size: frame.data.len(),
206 time: frame.time,
207 key: frame.key,
208 });
209 if out.len() >= want {
210 full = true;
211 }
212 }
213 Ok(())
214 }));
215 buf.drain(..fed.used);
216 if full {
217 break;
218 }
219 // No progress and nothing more to read means the file ended inside an
220 // element; no progress with more to read means the window must grow,
221 // which a block larger than the window legitimately requires.
222 if fed.used == 0 {
223 if done && fed.want > buf.len() {
224 break;
225 }
226 if fed.want > buf.len() {
227 let mut chunk = vec![0u8; fed.want - buf.len()];
228 let got = res!(file.read(&mut chunk));
229 if got == 0 {
230 break;
231 }
232 chunk.truncate(got);
233 buf.extend_from_slice(&chunk);
234 } else if done {
235 break;
236 }
237 }
238 }
239 Ok(Some(out))
240}
241
242/// Reads the same film's packets with ffprobe.
243fn read_theirs(path: &Path, want: usize, pick: &str) -> Option<Vec<Packet>> {
244 let out = match Command::new("ffprobe")
245 .args([
246 "-v", "error",
247 "-select_streams", pick,
248 "-show_entries", "packet=pts,size,flags",
249 "-of", "csv=p=0",
250 ])
251 .arg(path)
252 .output()
253 {
254 Ok(o) => o,
255 Err(_) => return None,
256 };
257 if !out.status.success() {
258 return None;
259 }
260 let text = String::from_utf8_lossy(&out.stdout);
261 let mut packets = Vec::new();
262 for line in text.lines() {
263 if packets.len() >= want {
264 break;
265 }
266 let mut parts = line.trim().split(',');
267 // A packet with no presentation time is one this comparison cannot
268 // speak about, and it is rare enough that giving up on the file is
269 // honest -- a partial list silently compared would be worse.
270 let time = match parts.next().map(|v| v.parse::<i64>()) {
271 Some(Ok(v)) => v,
272 _ => return None,
273 };
274 let size = match parts.next().map(|v| v.parse::<usize>()) {
275 Some(Ok(v)) => v,
276 _ => return None,
277 };
278 let flags = parts.next().unwrap_or("");
279 packets.push(Packet { size, time, key: flags.starts_with('K') });
280 }
281 Some(packets)
282}
283
284fn gather(dir: &Path, out: &mut Vec<PathBuf>) -> Outcome<()> {
285 let entries = match fs::read_dir(dir) {
286 Ok(e) => e,
287 Err(_) => return Ok(()),
288 };
289 for entry in entries.flatten() {
290 let path = entry.path();
291 // A dangling link is a fact about the collection, not a fault here.
292 let meta = match fs::metadata(&path) {
293 Ok(m) => m,
294 Err(_) => continue,
295 };
296 if meta.is_dir() {
297 res!(gather(&path, out));
298 } else if meta.len() > 0
299 && path.extension().map(|e| e.eq_ignore_ascii_case("mkv")).unwrap_or(false)
300 {
301 out.push(path);
302 }
303 }
304 Ok(())
305}
306
307fn num_from_env(key: &str, or: usize) -> usize {
308 match env::var(key) {
309 Ok(v) => v.parse::<usize>().unwrap_or(or),
310 Err(_) => or,
311 }
312}