Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/tests/h264_corpus.rs

22.0 KiB, 25 runs

created by r1870400018:21082, 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//! Hold the H.264 reader to a library of real films, and to FFmpeg.
2//!
3//! Nothing here uses a fixture. The films are somebody's actual photograph library, written by a
4//! dozen different phones and cameras over twenty years, and FFmpeg -- which has no idea this crate
5//! exists -- is the oracle for every number.
6//!
7//! Point `H264_FILMS` at a directory of films to run it:
8//!
9//! ```text
10//! H264_FILMS=/srv/nfs4/Gallery cargo test --release -p oxedyne_fe2o3_graphics --test h264_corpus \
11//! -- --nocapture
12//! ```
13//!
14//! Absent, each test says so rather than passing quietly: a check that skipped in silence would be
15//! a check nobody ran.
16//!
17//! `H264_LIMIT` caps how many films are read, for a quick pass; `H264_ONE` names a single file.
18//!
19//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
20//! Anthropic Claude
21
22use oxedyne_fe2o3_graphics::{
23 h264,
24 mp4::{
25 Film,
26 Kind,
27 },
28};
29
30use std::io::{
31 Read,
32 Seek,
33};
34
35use oxedyne_fe2o3_core::prelude::*;
36
37/// Unwraps an outcome, naming the film it came from.
38///
39/// `res!` carries error tags and not a message, and over a corpus of thousands of files the one
40/// thing a failure must say is *which file*, so this says it.
41macro_rules! at {
42 ($r:expr, $p:expr) => {
43 match $r {
44 Ok(v) => v,
45 Err(e) => return Err(err!("{:?}: {}", $p, e; Test, Invalid)),
46 }
47 };
48}
49
50/// Every film under the corpus directory, in a stable order.
51fn films() -> Outcome<Option<Vec<std::path::PathBuf>>> {
52 let root = match std::env::var("H264_FILMS") {
53 Ok(p) => p,
54 Err(_) => {
55 println!(" skipped: set H264_FILMS to a directory of films");
56 return Ok(None);
57 },
58 };
59 if let Ok(one) = std::env::var("H264_ONE") {
60 return Ok(Some(vec![std::path::PathBuf::from(one)]));
61 }
62 let mut out = Vec::new();
63 let mut stack = vec![std::path::PathBuf::from(&root)];
64 while let Some(dir) = stack.pop() {
65 let entries = match std::fs::read_dir(&dir) {
66 Ok(e) => e,
67 Err(_) => continue,
68 };
69 for entry in entries.flatten() {
70 let path = entry.path();
71 if path.is_dir() {
72 stack.push(path);
73 } else {
74 let ext = path.extension()
75 .map(|e| e.to_string_lossy().to_lowercase())
76 .unwrap_or_default();
77 if matches!(ext.as_str(), "mp4" | "mov" | "m4v") {
78 out.push(path);
79 }
80 }
81 }
82 }
83 out.sort();
84 if let Ok(n) = std::env::var("H264_LIMIT") {
85 if let Ok(n) = n.parse::<usize>() {
86 out.truncate(n);
87 }
88 }
89 Ok(Some(out))
90}
91
92
93/// Reads a film's index, and one sample of it, without holding the film.
94///
95/// The corpus runs to 91 gigabytes and one film in it is four gigabytes on its own, so nothing here
96/// reads a whole file. The top-level boxes are walked by seeking over each one's header, the `moov`
97/// box alone is read, and then one sample is read from the span the index names. That is also the
98/// shape a photograph library wants: a thumbnail should cost the metadata and one frame.
99fn open(path: &std::path::Path) -> Outcome<Option<(Film, Vec<u8>)>> {
100 let mut f = res!(std::fs::File::open(path));
101 let len = res!(f.metadata()).len();
102 let mut at = 0u64;
103 let mut moov: Option<Vec<u8>> = None;
104 while at + 8 <= len {
105 res!(f.seek(std::io::SeekFrom::Start(at)));
106 let mut head = [0u8; 16];
107 if res!(f.read(&mut head[..8])) < 8 {
108 break;
109 }
110 let short = u32::from_be_bytes([head[0], head[1], head[2], head[3]]);
111 let (size, hlen) = match short {
112 1 => {
113 if res!(f.read(&mut head[8..16])) < 8 {
114 break;
115 }
116 let mut wide = [0u8; 8];
117 wide.copy_from_slice(&head[8..16]);
118 (u64::from_be_bytes(wide), 16u64)
119 },
120 0 => (len - at, 8u64),
121 n => (n as u64, 8u64),
122 };
123 if size < hlen || at + size > len {
124 break;
125 }
126 if &head[4..8] == b"moov" {
127 let mut body = vec![0u8; (size - hlen) as usize];
128 res!(f.seek(std::io::SeekFrom::Start(at + hlen)));
129 res!(f.read_exact(&mut body));
130 moov = Some(body);
131 break;
132 }
133 at += size;
134 }
135 let moov = match moov {
136 Some(m) => m,
137 None => return Ok(None),
138 };
139 let film = res!(Film::from_moov(&moov));
140 let i = res!(film.first_sync());
141 let (off, size) = res!(film.span(i));
142 let mut sample = vec![0u8; size as usize];
143 res!(f.seek(std::io::SeekFrom::Start(off)));
144 res!(f.read_exact(&mut sample));
145 Ok(Some((film, sample)))
146}
147
148/// What FFmpeg's own header tracer says about a film's parameter sets.
149///
150/// `trace_headers` prints every syntax element of the sequence and picture parameter sets by name,
151/// which is a wholly separate parse of the same bytes by a wholly separate implementation. That is
152/// the point: a parser checked against itself proves nothing.
153fn ffmpeg_headers(path: &std::path::Path) -> Outcome<std::collections::HashMap<String, i64>> {
154 let out = res!(std::process::Command::new("ffmpeg")
155 .args(["-v", "trace", "-i"])
156 .arg(path)
157 .args(["-map", "0:v:0", "-c:v", "copy", "-bsf:v", "trace_headers", "-frames:v", "1",
158 "-f", "null", "-"])
159 .output());
160 let text = String::from_utf8_lossy(&out.stderr);
161 let mut got = std::collections::HashMap::new();
162 for line in text.lines() {
163 // Each traced field is `<bit position> <name> <bits> = <value>`, and the first occurrence
164 // of a name is the one in the first parameter set of its kind.
165 // A traced field reads `[trace_headers @ 0x..] <bit> <name> <bits> = <value>`, so the
166 // name is the token before the bit pattern on the left of the equals sign.
167 let (left, value) = match line.split_once(" = ") {
168 Some((l, v)) => (l, v.trim()),
169 None => continue,
170 };
171 let mut tail = left.split_whitespace().rev();
172 let _bits = tail.next();
173 let name = match tail.next() {
174 Some(n) => n,
175 None => continue,
176 };
177 if let Ok(v) = value.parse::<i64>() {
178 got.entry(name.to_string()).or_insert(v);
179 }
180 }
181 Ok(got)
182}
183
184#[test]
185fn test_every_parameter_set_reads_as_ffmpeg_reads_it_01() -> Outcome<()> {
186 // The whole header layer, held to a separate implementation over the whole corpus. A field read
187 // one bit out of place still yields a plausible number, and the only way to catch that is to
188 // ask somebody else what the number is.
189 let films = match res!(films()) {
190 Some(f) => f,
191 None => return Ok(()),
192 };
193 let mut avc = 0usize;
194 let mut other = 0usize;
195 let mut checked = 0usize;
196 for path in &films {
197 let (film, sample) = match at!(open(path), path) {
198 Some(f) => f,
199 None => continue,
200 };
201 let _ = &sample;
202 if film.kind() != Kind::Avc {
203 other += 1;
204 continue;
205 }
206 avc += 1;
207 let cfg = at!(h264::config(film.config()), path);
208 let mut sets = Vec::new();
209 for u in &cfg.sps {
210 sets.push(at!(h264::sps(&u.body), path));
211 }
212 let mut pics = Vec::new();
213 for u in &cfg.pps {
214 pics.push(at!(h264::pps(&u.body, &sets), path));
215 }
216 let (s, p) = match (sets.first(), pics.first()) {
217 (Some(s), Some(p)) => (s, p),
218 _ => return Err(err!(
219 "{:?} carries {} sequence and {} picture parameter sets, and a film needs one of \
220 each.", path, sets.len(), pics.len(); Test, Invalid)),
221 };
222 let ff = res!(ffmpeg_headers(path));
223 if ff.is_empty() {
224 return Err(err!(
225 "FFmpeg traced no headers for {:?}, so there is nothing to check against.", path;
226 Test, Missing));
227 }
228 // Every field both implementations name, what each of ours holds, and -- for the fields a
229 // parameter set may leave out -- what §7.4.2.1.1 says is inferred when it does. A field
230 // FFmpeg does not trace is a field the set did not carry, and this decoder must then be
231 // holding the inferred value rather than a value of its own.
232 let mine: Vec<(&str, i64, Option<i64>)> = vec![
233 ("profile_idc", s.profile as i64, None),
234 ("level_idc", s.level as i64, None),
235 ("chroma_format_idc", s.chroma as i64, Some(1)),
236 ("bit_depth_luma_minus8", s.luma_bits as i64 - 8, Some(0)),
237 ("bit_depth_chroma_minus8", s.chroma_bits as i64 - 8, Some(0)),
238 ("log2_max_frame_num_minus4", s.frame_num_bits as i64 - 4, None),
239 ("pic_order_cnt_type", s.poc_type as i64, None),
240 ("pic_width_in_mbs_minus1", s.mbs_w as i64 - 1, None),
241 ("pic_height_in_map_units_minus1", s.map_units_h as i64 - 1, None),
242 ("frame_mbs_only_flag", s.frame_mbs_only as i64, None),
243 ("entropy_coding_mode_flag", p.cabac as i64, None),
244 ("num_slice_groups_minus1", p.slice_groups as i64 - 1, None),
245 ("pic_init_qp_minus26", p.init_qp as i64 - 26, None),
246 ("chroma_qp_index_offset", p.cb_qp_offset as i64, None),
247 ("constrained_intra_pred_flag", p.constrained_intra as i64, None),
248 ("deblocking_filter_control_present_flag", p.deblocking_control as i64, None),
249 ("transform_8x8_mode_flag", p.transform_8x8 as i64, Some(0)),
250 ("second_chroma_qp_index_offset", p.cr_qp_offset as i64, Some(p.cb_qp_offset as i64)),
251 ];
252 for (name, held, inferred) in mine {
253 let theirs = match (ff.get(name), inferred) {
254 (Some(v), _) => *v,
255 (None, Some(d)) if held == d => continue,
256 (None, Some(d)) => return Err(err!(
257 "{:?}: {} is absent from the set, so it is inferred as {}, and this decoder \
258 holds {}.", path, name, d, held; Test, Mismatch)),
259 (None, None) => return Err(err!(
260 "{:?}: this decoder reads {} as {} and FFmpeg does not name it at all.",
261 path, name, held; Test, Mismatch)),
262 };
263 if theirs != held {
264 return Err(err!(
265 "{:?}: this decoder reads {} as {} and FFmpeg reads it as {}.",
266 path, name, held, theirs; Test, Mismatch));
267 }
268 checked += 1;
269 }
270 }
271 println!(" {} H.264 films, {} fields agreed with FFmpeg; {} films of other codecs",
272 avc, checked, other);
273 if avc == 0 {
274 return Err(err!("Not one H.264 film was found, so nothing was checked."; Test, Missing));
275 }
276 Ok(())
277}
278
279#[test]
280fn test_every_film_gives_up_its_first_sample_02() -> Outcome<()> {
281 // The container half. A sample table read wrongly hands the decoder somebody else's bytes, and
282 // the check that catches it is that the first sample begins with a NAL unit of the type the
283 // stream says it should.
284 let films = match res!(films()) {
285 Some(f) => f,
286 None => return Ok(()),
287 };
288 let mut counts = std::collections::BTreeMap::new();
289 let mut idrs = 0usize;
290 for path in &films {
291 let (film, sample) = match at!(open(path), path) {
292 Some(f) => f,
293 None => {
294 *counts.entry(fmt!("no video track")).or_insert(0usize) += 1;
295 continue;
296 },
297 };
298 *counts.entry(fmt!("{:?}", film.kind())).or_insert(0usize) += 1;
299 if film.kind() != Kind::Avc {
300 continue;
301 }
302 let cfg = at!(h264::config(film.config()), path);
303 let units = at!(h264::split_lengthed(&sample, cfg.length_size), path);
304 let slices: Vec<&h264::Unit> = units.iter()
305 .filter(|u| matches!(u.kind, h264::nal::SLICE | h264::nal::IDR))
306 .collect();
307 if slices.is_empty() {
308 return Err(err!(
309 "{:?}: the first sample holds no coded slice, only NAL units {:?}.",
310 path, units.iter().map(|u| u.kind).collect::<Vec<_>>(); Test, Invalid));
311 }
312 if slices.iter().all(|u| u.kind == h264::nal::IDR) {
313 idrs += 1;
314 }
315 }
316 println!(" {:?}; {} films whose first sample is an IDR", counts, idrs);
317 Ok(())
318}
319
320#[test]
321fn test_every_first_slice_header_reads_03() -> Outcome<()> {
322 // The slice header is where a misread costs most: it ends at the first bit of the entropy-coded
323 // data, so a field read one bit out puts the whole picture's decode one bit out. There is no
324 // oracle for its length in FFmpeg's trace, so what is asserted here is what can be: that every
325 // header in the corpus reads to completion, that every slice is intra, and that the slices of a
326 // picture between them cover every macroblock in it exactly once.
327 let films = match res!(films()) {
328 Some(f) => f,
329 None => return Ok(()),
330 };
331 let mut headers = 0usize;
332 let mut multi = 0usize;
333 for path in &films {
334 let (film, sample) = match at!(open(path), path) {
335 Some(f) => f,
336 None => continue,
337 };
338 if film.kind() != Kind::Avc {
339 continue;
340 }
341 let cfg = at!(h264::config(film.config()), path);
342 let mut sets = Vec::new();
343 for u in &cfg.sps {
344 sets.push(at!(h264::sps(&u.body), path));
345 }
346 let mut pics = Vec::new();
347 for u in &cfg.pps {
348 pics.push(at!(h264::pps(&u.body, &sets), path));
349 }
350 let units = at!(h264::split_lengthed(&sample, cfg.length_size), path);
351 let mut firsts = Vec::new();
352 for u in &units {
353 // A sample may carry its own parameter sets, which override the record's.
354 match u.kind {
355 h264::nal::SPS => {
356 let s = at!(h264::sps(&u.body), path);
357 sets.retain(|o| o.id != s.id);
358 sets.push(s);
359 },
360 h264::nal::PPS => {
361 let p = at!(h264::pps(&u.body, &sets), path);
362 pics.retain(|o| o.id != p.id);
363 pics.push(p);
364 },
365 h264::nal::SLICE | h264::nal::IDR => {
366 let sh = at!(h264::slice(u, &sets, &pics), path);
367 let intra = sh.kind.is_intra();
368 req!(intra, true, "{:?} opens with a {:?} slice", path, sh.kind);
369 firsts.push(sh.first_mb);
370 headers += 1;
371 },
372 _ => {},
373 }
374 }
375 if firsts.len() > 1 {
376 multi += 1;
377 }
378 // The slices must tile the picture: the first begins at macroblock nought, and each one
379 // after it begins after the one before. A header read one field out of place shows up here
380 // as a first macroblock that is enormous or out of order.
381 let sps = match sets.first() {
382 Some(s) => s,
383 None => continue,
384 };
385 let total = sps.mbs_w * sps.map_units_h;
386 let mut sorted = firsts.clone();
387 sorted.sort_unstable();
388 req!(sorted, firsts, "{:?}: the slices arrive out of order", path);
389 req!(firsts.first().copied().unwrap_or(u32::MAX), 0u32,
390 "{:?}: the first slice does not begin at macroblock nought", path);
391 for f in &firsts {
392 let inside = *f < total;
393 req!(inside, true,
394 "{:?}: a slice begins at macroblock {} of a picture that holds {}",
395 path, f, total);
396 }
397 }
398 println!(" {} slice headers read; {} pictures of more than one slice", headers, multi);
399 Ok(())
400}
401
402/// FFmpeg's own decode of a film's first intra-coded frame, as raw 4:2:0 planes.
403///
404/// `skip_filter` asks FFmpeg to leave the deblocking filter out, which is how a fault in prediction
405/// or in the residual is told apart from a fault in the filter.
406///
407/// **The picture asked for is the first *intra* one, not the first one shown.** A decoder that draws
408/// a film's opening frame decodes the first sync sample, which is an IDR; where a film is coded with
409/// bidirectional prediction that picture is not the first in presentation order, and pictures shown
410/// before it refer forward to it. Nine films in the corpus are like that -- 4K, all-intra pictures
411/// interleaved with B pictures -- and without this the two sides decode different moments of the same
412/// wedding and disagree in every sample, with nothing to say why.
413fn ffmpeg_frame(path: &std::path::Path, skip_filter: bool) -> Outcome<Vec<u8>> {
414 let mut cmd = std::process::Command::new("ffmpeg");
415 // **`-noautorotate` is load-bearing.** A phone writes the rotation it was held at into the
416 // track header, and FFmpeg turns the picture on the way out. A decoder produces the picture as
417 // it was coded, and rotation is the container's business, so without this the two are compared
418 // after one of them has been turned -- and at ninety degrees the byte count is unchanged, so
419 // nothing but the samples says so.
420 //
421 // **`-apply_cropping:v codec` is load-bearing for the same reason.** A film may carry a clean
422 // aperture in its sample description, and FFmpeg crops to it by default; a decoder produces the
423 // picture the *coded* stream describes, cropped to the sequence parameter set's conformance
424 // window and nothing else. One film in the corpus is coded 1440 by 1080 and asks in its
425 // container to be shown as 1308 by 980, and without this the two are compared after one of them
426 // has been cut.
427 cmd.args(["-v", "error", "-noautorotate", "-apply_cropping:v", "codec"]);
428 if skip_filter {
429 cmd.args(["-skip_loop_filter", "all"]);
430 }
431 cmd.arg("-i").arg(path)
432 .args(["-map", "0:v:0", "-vf", "select='eq(pict_type\\,I)'", "-frames:v", "1",
433 "-pix_fmt", "yuv420p", "-f", "rawvideo", "-"]);
434 let out = res!(cmd.output());
435 if out.stdout.is_empty() {
436 return Err(err!(
437 "FFmpeg decoded no frame of {:?}: {}", path, String::from_utf8_lossy(&out.stderr);
438 Test, Missing));
439 }
440 Ok(out.stdout)
441}
442
443/// Compares one decoded plane against the matching run of FFmpeg's output, giving the count that
444/// differ, the worst difference, and where the first one is.
445fn compare(mine: &[u8], theirs: &[u8], w: usize) -> (usize, i32, Option<(usize, usize)>) {
446 let mut wrong = 0usize;
447 let mut worst = 0i32;
448 let mut first = None;
449 for (i, (a, b)) in mine.iter().zip(theirs.iter()).enumerate() {
450 let d = (*a as i32 - *b as i32).abs();
451 if d != 0 {
452 wrong += 1;
453 worst = worst.max(d);
454 if first.is_none() {
455 first = Some((i % w, i / w));
456 }
457 }
458 }
459 (wrong, worst, first)
460}
461
462#[test]
463fn test_the_first_frame_matches_ffmpeg_04() -> Outcome<()> {
464 // The only check that matters. Every table, every prediction mode, every shift in the inverse
465 // transform and every threshold in the deblocking filter has to be right at once, or some
466 // sample somewhere differs -- and a decoder that is nearly right produces a picture that looks
467 // decoded, so nothing short of sample-for-sample agreement is evidence of anything.
468 //
469 // `H264_UNFILTERED` compares against a decode with the deblocking filter left out of both,
470 // which says whether a mismatch is in the prediction and residual or in the filter.
471 let films = match res!(films()) {
472 Some(f) => f,
473 None => return Ok(()),
474 };
475 let unfiltered = std::env::var("H264_UNFILTERED").is_ok();
476 let mut exact = 0usize;
477 let mut differ: Vec<String> = Vec::new();
478 let mut refused: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
479 let mut tried = 0usize;
480 for path in &films {
481 let (film, sample) = match at!(open(path), path) {
482 Some(f) => f,
483 None => continue,
484 };
485 if film.kind() != Kind::Avc {
486 continue;
487 }
488 tried += 1;
489 let mine = if unfiltered {
490 h264::decode::picture_undeblocked(film.config(), &sample)
491 } else {
492 h264::decode::picture(film.config(), &sample)
493 };
494 let mine = match mine {
495 Ok(p) => p,
496 Err(e) => {
497 // A refusal is an outcome, not a failure: what matters is that it is refused by
498 // name rather than decoded into a wrong picture.
499 let text = fmt!("{}", e);
500 let line = text.lines().last().unwrap_or("?").to_string();
501 *refused.entry(line).or_insert(0) += 1;
502 continue;
503 },
504 };
505 let theirs = res!(ffmpeg_frame(path, unfiltered));
506 let (w, h) = (mine.y.w, mine.y.h);
507 let luma = w * h;
508 let chroma = (w / 2) * (h / 2);
509 if theirs.len() != luma + 2 * chroma {
510 // A disagreement about the *size* is as much a difference as a disagreement about a
511 // sample, and it is recorded as one rather than ending the survey: one odd film would
512 // otherwise stop every film after it from being measured at all.
513 differ.push(fmt!(
514 "{:?} {}x{}: this decoder made {} bytes of picture and FFmpeg gave {}",
515 path.file_name().unwrap_or_default(), w, h, luma + 2 * chroma, theirs.len()));
516 continue;
517 }
518 let (wy, dy, fy) = compare(&mine.y.px, &theirs[..luma], w);
519 let (wu, du, _) = compare(&mine.cb.px, &theirs[luma..luma + chroma], w / 2);
520 let (wv, dv, _) = compare(&mine.cr.px, &theirs[luma + chroma..], w / 2);
521 if wy == 0 && wu == 0 && wv == 0 {
522 exact += 1;
523 } else {
524 differ.push(fmt!(
525 "{:?} {}x{}: luma {} wrong (worst {}) first at {:?}, Cb {} (worst {}), \
526 Cr {} (worst {})",
527 path.file_name().unwrap_or_default(), w, h, wy, dy, fy, wu, du, wv, dv));
528 }
529 }
530 println!(" {} H.264 films tried, {} matched FFmpeg exactly", tried, exact);
531 if !refused.is_empty() {
532 println!(" refused:");
533 for (why, n) in &refused {
534 println!(" {} x {}", n, why.trim());
535 }
536 }
537 for line in &differ {
538 println!(" DIFFERS {}", line);
539 }
540 if !differ.is_empty() {
541 return Err(err!(
542 "{} of {} films decoded to something other than what FFmpeg decoded. A picture that \
543 is nearly right is a wrong picture.", differ.len(), tried; Test, Mismatch));
544 }
545 if exact == 0 && tried > 0 {
546 return Err(err!(
547 "Not one of {} films matched FFmpeg, so nothing was proved.", tried; Test, Mismatch));
548 }
549 Ok(())
550}
551
552#[test]
553fn test_zz_a_window_onto_one_film_05() -> Outcome<()> {
554 // Not a check but a window, and it earned its place: every fault found in this decoder was
555 // found by looking through it. Point `H264_DUMP` at one film and it prints that film's
556 // parameter sets, its NAL units, its slice headers, and an eight-by-eight patch of luma beside
557 // FFmpeg's -- `H264_X0` and `H264_Y0` move the patch, `H264_UNFILTERED` takes the deblocking
558 // filter out of both sides.
559 //
560 // The point of the patch is that a whole-frame difference count says nothing about *what* went
561 // wrong, while eight rows of samples beside eight rows of somebody else's say a great deal: a
562 // prediction using the wrong direction, a coefficient at the wrong frequency and a filter run
563 // one sample too deep all look different from each other here, and identical in a count.
564 let one = match std::env::var("H264_DUMP") {
565 Ok(p) => std::path::PathBuf::from(p),
566 Err(_) => return Ok(()),
567 };
568 let (film, sample) = match at!(open(&one), &one) {
569 Some(f) => f,
570 None => return Err(err!("no video track"; Test, Missing)),
571 };
572 let cfg = at!(h264::config(film.config()), &one);
573 let mut sets = Vec::new();
574 for u in &cfg.sps {
575 sets.push(at!(h264::sps(&u.body), &one));
576 }
577 let mut pics = Vec::new();
578 for u in &cfg.pps {
579 pics.push(at!(h264::pps(&u.body, &sets), &one));
580 }
581 println!(" sps: {:?}", sets.first());
582 println!(" pps: {:?}", pics.first());
583 let units = at!(h264::split_lengthed(&sample, cfg.length_size), &one);
584 for u in &units {
585 println!(" nal {} ref {} len {}", u.kind, u.ref_idc, u.body.len());
586 if matches!(u.kind, h264::nal::SLICE | h264::nal::IDR) {
587 println!(" slice: {:?}", at!(h264::slice(u, &sets, &pics), &one));
588 }
589 }
590 let unfiltered = std::env::var("H264_UNFILTERED").is_ok();
591 let mine = if unfiltered {
592 at!(h264::decode::picture_undeblocked(film.config(), &sample), &one)
593 } else {
594 at!(h264::decode::picture(film.config(), &sample), &one)
595 };
596 let theirs = res!(ffmpeg_frame(&one, unfiltered));
597 let w = mine.y.w;
598 let x0: usize = std::env::var("H264_X0").ok().and_then(|v| v.parse().ok()).unwrap_or(0);
599 let y0: usize = std::env::var("H264_Y0").ok().and_then(|v| v.parse().ok()).unwrap_or(0);
600 for y in y0..y0 + 8 {
601 let a: Vec<u8> = (x0..x0 + 8).map(|x| mine.y.px[y * w + x]).collect();
602 let b: Vec<u8> = (x0..x0 + 8).map(|x| theirs[y * w + x]).collect();
603 println!(" row {}: mine {:?}", y, a);
604 println!(" ffmg {:?}", b);
605 }
606 Ok(())
607}