Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/tests/h264_films.rs

7.0 KiB, 3 runs

created by r1870400018:21316, 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//! How many of a library's H.264 films can the decoder draw the first frame of?
2//!
3//! Two films in every five in the survey corpus are coded with the variable-length entropy coder and
4//! three in five with the arithmetic one, and until both were written only the first kind could be
5//! drawn at all. This asks the question of the real corpus and **reports** the answer rather than
6//! asserting one, because "how many films can be drawn today" is a measurement and not a promise.
7//! What it does assert is that nothing is refused silently: every refusal is counted and named, and
8//! a picture of no size is a refusal rather than a success.
9//!
10//! There is no oracle here. Agreement with FFmpeg, sample for sample, is `h264_corpus.rs`'s job;
11//! this one says how far the decoder reaches.
12//!
13//! ```text
14//! H264_FILMS=/srv/nfs4/Gallery cargo test --release -p oxedyne_fe2o3_graphics --test h264_films \
15//! -- --nocapture
16//! ```
17//!
18//! Absent, it says so rather than passing quietly. `H264_FILMS_MAX` caps how many films are read.
19//!
20//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
21//! Anthropic Claude
22
23use oxedyne_fe2o3_graphics::{
24 h264,
25 mp4::{
26 Film,
27 Kind,
28 },
29};
30
31use std::{
32 collections::BTreeMap,
33 env,
34 fs,
35 io::{
36 Read,
37 Seek,
38 },
39 path::PathBuf,
40};
41
42use oxedyne_fe2o3_core::prelude::*;
43
44/// Every film under a directory, in a stable order.
45fn films(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
46 let entries = match fs::read_dir(dir) {
47 Ok(e) => e,
48 Err(_) => return,
49 };
50 let mut here = Vec::new();
51 for entry in entries.flatten() {
52 let path = entry.path();
53 if path.is_dir() {
54 films(&path, out);
55 } else {
56 let name = path.to_string_lossy().to_lowercase();
57 if name.ends_with(".mp4") || name.ends_with(".mov") || name.ends_with(".m4v") {
58 here.push(path);
59 }
60 }
61 }
62 here.sort();
63 out.extend(here);
64}
65
66/// Reads a film's index, and one sample of it, without holding the film.
67///
68/// The corpus runs to 91 gigabytes and one film in it is four gigabytes on its own, so nothing here
69/// reads a whole file: the top-level boxes are walked by seeking over each one's header, the `moov`
70/// box alone is read, and then one sample is read from the span the index names.
71fn open(path: &std::path::Path) -> Outcome<Option<(Film, Vec<u8>)>> {
72 let mut f = res!(fs::File::open(path));
73 let len = res!(f.metadata()).len();
74 let mut at = 0u64;
75 let mut moov: Option<Vec<u8>> = None;
76 while at + 8 <= len {
77 res!(f.seek(std::io::SeekFrom::Start(at)));
78 let mut head = [0u8; 16];
79 if res!(f.read(&mut head[..8])) < 8 {
80 break;
81 }
82 let short = u32::from_be_bytes([head[0], head[1], head[2], head[3]]);
83 let (size, hlen) = match short {
84 1 => {
85 if res!(f.read(&mut head[8..16])) < 8 {
86 break;
87 }
88 let mut wide = [0u8; 8];
89 wide.copy_from_slice(&head[8..16]);
90 (u64::from_be_bytes(wide), 16u64)
91 },
92 0 => (len - at, 8u64),
93 n => (n as u64, 8u64),
94 };
95 if size < hlen || at + size > len {
96 break;
97 }
98 if &head[4..8] == b"moov" {
99 let mut body = vec![0u8; (size - hlen) as usize];
100 res!(f.seek(std::io::SeekFrom::Start(at + hlen)));
101 res!(f.read_exact(&mut body));
102 moov = Some(body);
103 break;
104 }
105 at += size;
106 }
107 let moov = match moov {
108 Some(m) => m,
109 None => return Ok(None),
110 };
111 let film = res!(Film::from_moov(&moov));
112 let i = res!(film.first_sync());
113 let (off, size) = res!(film.span(i));
114 let mut sample = vec![0u8; size as usize];
115 res!(f.seek(std::io::SeekFrom::Start(off)));
116 res!(f.read_exact(&mut sample));
117 Ok(Some((film, sample)))
118}
119
120/// The first sentence of a refusal, which is what distinguishes one from another.
121///
122/// The tail names the file, and keeping it would make every refusal look distinct and the count
123/// useless.
124fn why(e: &Error<ErrTag>) -> String {
125 let text = fmt!("{}", e.plain());
126 let head = text.split(". ").next().unwrap_or(&text).trim().to_string();
127 // A message that runs over several lines in the source arrives with its indentation in it.
128 head.split_whitespace().collect::<Vec<_>>().join(" ")
129}
130
131#[test]
132fn how_many_films_the_decoder_can_draw() -> Outcome<()> {
133 let dir = match env::var("H264_FILMS") {
134 Ok(d) if !d.is_empty() => PathBuf::from(d),
135 _ => {
136 println!("skipped: set H264_FILMS to a directory of films");
137 return Ok(());
138 },
139 };
140 let cap = match env::var("H264_FILMS_MAX") {
141 Ok(n) => res!(n.parse::<usize>()),
142 Err(_) => usize::MAX,
143 };
144
145 let mut all = Vec::new();
146 films(&dir, &mut all);
147 println!("{} films under {}", all.len(), dir.display());
148
149 let mut avc = 0usize;
150 let mut drawn = 0usize;
151 let mut cabac = 0usize;
152 let mut cabac_drawn = 0usize;
153 let mut refused: BTreeMap<String, usize> = BTreeMap::new();
154 let mut looked = 0usize;
155
156 for path in &all {
157 if looked >= cap {
158 break;
159 }
160 let (film, sample) = match open(path) {
161 Ok(Some(f)) => f,
162 Ok(None) => continue,
163 Err(e) => {
164 *refused.entry(why(&e)).or_insert(0) += 1;
165 continue;
166 },
167 };
168 if film.kind() != Kind::Avc {
169 continue;
170 }
171 avc += 1;
172 looked += 1;
173 // Which entropy coder the film uses, which is the split this decoder was widened for. A
174 // film whose parameter sets will not read at all is counted as a refusal below rather than
175 // here.
176 let arithmetic = match h264::config(film.config()) {
177 Ok(cfg) => {
178 let mut sets = Vec::new();
179 for u in &cfg.sps {
180 if let Ok(s) = h264::sps(&u.body) {
181 sets.push(s);
182 }
183 }
184 cfg.pps.iter()
185 .filter_map(|u| h264::pps(&u.body, &sets).ok())
186 .any(|p| p.cabac)
187 },
188 Err(_) => false,
189 };
190 if arithmetic {
191 cabac += 1;
192 }
193 match h264::decode::picture(film.config(), &sample) {
194 Ok(pic) => {
195 // A picture, and one of the size the parameter sets promised: a decoder that
196 // answered an empty frame would otherwise count as a success here.
197 if pic.y.w == 0 || pic.y.h == 0 {
198 *refused.entry("an empty picture".to_string()).or_insert(0) += 1;
199 } else {
200 drawn += 1;
201 if arithmetic {
202 cabac_drawn += 1;
203 }
204 }
205 },
206 Err(e) => {
207 *refused.entry(why(&e)).or_insert(0) += 1;
208 },
209 }
210 }
211
212 println!("{} H.264 films looked at, {} first frames drawn", avc, drawn);
213 if avc > 0 {
214 println!("that is {:.1}% of them", 100.0 * drawn as f64 / avc as f64);
215 }
216 println!("{} of them are coded with the arithmetic entropy coder, {} drawn", cabac, cabac_drawn);
217 println!("{} with the variable-length one, {} drawn", avc - cabac, drawn - cabac_drawn);
218 // Every refusal, by name and by count. Nothing is folded into an "other": a refusal nobody can
219 // read is a refusal nobody will fix.
220 let mut sorted: Vec<(String, usize)> = refused.into_iter().collect();
221 sorted.sort_by(|a, b| b.1.cmp(&a.1));
222 println!("refused:");
223 if sorted.is_empty() {
224 println!(" - nothing");
225 }
226 let mut total = 0usize;
227 for (name, n) in &sorted {
228 println!(" {:>5} {}", n, name);
229 total += n;
230 }
231 // The one thing asserted: every film is accounted for, so a film that vanished between the walk
232 // and the count would show up here rather than as a quietly smaller total.
233 req!(drawn + total, avc,
234 "{} films drawn and {} refused, out of {} looked at", drawn, total, avc);
235 Ok(())
236}