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 | |
| 23 | use oxedyne_fe2o3_graphics::{ |
| 24 | h264, |
| 25 | mp4::{ |
| 26 | Film, |
| 27 | Kind, |
| 28 | }, |
| 29 | }; |
| 30 | |
| 31 | use std::{ |
| 32 | collections::BTreeMap, |
| 33 | env, |
| 34 | fs, |
| 35 | io::{ |
| 36 | Read, |
| 37 | Seek, |
| 38 | }, |
| 39 | path::PathBuf, |
| 40 | }; |
| 41 | |
| 42 | use oxedyne_fe2o3_core::prelude::*; |
| 43 | |
| 44 | /// Every film under a directory, in a stable order. |
| 45 | fn 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. |
| 71 | fn 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. |
| 124 | fn 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] |
| 132 | fn 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 | } |