oxedyne/fe2o3/fe2o3_graphics/src/avi.rs
11.2 KiB, 37 runs
created by r1870400018:21353, 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 | //! AVI: the size and running time of a film in a RIFF container. |
| 2 | //! |
| 3 | //! AVI predates the ISO base media format that [`crate::mp4`] reads, and a |
| 4 | //! camera of the late 2000s wrote it by default. The files are still in family |
| 5 | //! libraries, and a photo library that cannot see them under-reports what is on |
| 6 | //! the disk. |
| 7 | //! |
| 8 | //! This reads the header and nothing else. **No AVI is decoded here**: what is |
| 9 | //! inside one is usually Motion JPEG or DV, and a caller wanting a frame either |
| 10 | //! has a decoder for the stream's own codec or has none. What a catalogue needs |
| 11 | //! is how big the picture is, how long it runs and what it is coded in, and all |
| 12 | //! three are in the header list at the front of the file -- so a head of a few |
| 13 | //! kilobytes answers them without opening the rest. |
| 14 | //! |
| 15 | //! # The shape of the file |
| 16 | //! |
| 17 | //! A RIFF file is `RIFF`, a length, a form type, and then a sequence of chunks, |
| 18 | //! each a four-character code, a length, and that many bytes. A `LIST` chunk |
| 19 | //! begins with a further four-character code and then holds chunks of its own. |
| 20 | //! An AVI's form type is `AVI ` -- with the trailing space, which is not a |
| 21 | //! typographic accident -- and the first `LIST` is `hdrl`, holding: |
| 22 | //! |
| 23 | //! - `avih`, the main header: the size of the picture, how many frames there |
| 24 | //! are and how long each is shown. |
| 25 | //! - one `LIST strl` a stream, each opening with `strh`, the stream header, |
| 26 | //! which says whether the stream is video and what codec it carries. |
| 27 | //! |
| 28 | //! # References |
| 29 | //! |
| 30 | //! Microsoft's AVI RIFF File Reference for `avih` (`AVIMAINHEADER`) and `strh` |
| 31 | //! (`AVISTREAMHEADER`), and the OpenDML AVI File Format Extensions v1.02 for why |
| 32 | //! the main header's frame count is not to be trusted on its own. |
| 33 | //! |
| 34 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 35 | //! Anthropic Claude |
| 36 | |
| 37 | use oxedyne_fe2o3_core::prelude::*; |
| 38 | |
| 39 | const VIDEO: &[u8; 4] = b"vids"; // a strh's stream type, for video |
| 40 | |
| 41 | // The header list is at the front, so a file that has not shown it by here is |
| 42 | // not one this reader understands. Without a bound, a length field of nought -- |
| 43 | // which a truncated or malformed file readily supplies -- is an endless walk. |
| 44 | const CHUNK_LIMIT: usize = 64; |
| 45 | |
| 46 | /// What a film's header says about it. |
| 47 | #[derive(Clone, Debug, Default, Eq, PartialEq)] |
| 48 | pub struct Avi { |
| 49 | w: u32, // picture width, pixels |
| 50 | h: u32, // picture height, pixels |
| 51 | micros: u32, // microseconds a frame is shown, from the main header |
| 52 | frames: u32, // frame count the main header claims |
| 53 | // Frame rate |
| 54 | rate: u32, // quotient with scale is frames a second |
| 55 | scale: u32, |
| 56 | length: u32, // frame count the video stream's own header claims |
| 57 | codec: [u8; 4], // four-character code of the video stream's codec |
| 58 | } |
| 59 | |
| 60 | impl Avi { |
| 61 | |
| 62 | /// The buffer need not be the whole film -- the header list is at the front |
| 63 | /// -- and a chunk running past the end of what is in hand simply ends the |
| 64 | /// walk, so a caller holding a sniffing buffer gets the same answer as one |
| 65 | /// holding the file. |
| 66 | pub fn read(bytes: &[u8]) -> Outcome<Self> { |
| 67 | if !is_avi(bytes) { |
| 68 | return Err(err!( |
| 69 | "Not an AVI: a RIFF file whose form type is 'AVI ' was expected."; |
| 70 | Invalid, Input, Format)); |
| 71 | } |
| 72 | let mut out = Self::default(); |
| 73 | // Past `RIFF`, its length, and the form type. |
| 74 | res!(out.walk(&bytes[12..], 0)); |
| 75 | if out.w == 0 || out.h == 0 { |
| 76 | return Err(err!( |
| 77 | "The AVI header list gave no picture size."; |
| 78 | Invalid, Input, Missing)); |
| 79 | } |
| 80 | Ok(out) |
| 81 | } |
| 82 | |
| 83 | /// Every `LIST` is descended into, and `depth` bounds that descent: a `LIST` |
| 84 | /// claiming to contain itself is a file that would otherwise be walked for |
| 85 | /// ever. |
| 86 | fn walk(&mut self, mut at: &[u8], depth: usize) -> Outcome<()> { |
| 87 | if depth > 3 { |
| 88 | return Ok(()); |
| 89 | } |
| 90 | let mut seen = 0usize; |
| 91 | while at.len() >= 8 && seen < CHUNK_LIMIT { |
| 92 | seen += 1; |
| 93 | let id = &at[..4]; |
| 94 | let len = u32_at(at, 4) as usize; |
| 95 | let body = &at[8..]; |
| 96 | // A chunk longer than what is in hand is not a fault: the caller may |
| 97 | // be holding only the front of the file. |
| 98 | let take = len.min(body.len()); |
| 99 | match id { |
| 100 | b"LIST" if take >= 4 => res!(self.walk(&body[4..take], depth + 1)), |
| 101 | b"avih" => self.read_avih(&body[..take]), |
| 102 | b"strh" => self.read_strh(&body[..take]), |
| 103 | _ => {}, |
| 104 | } |
| 105 | // Chunks are padded to an even length, and the pad byte is not |
| 106 | // counted in the length -- a reader that forgets it is one byte out |
| 107 | // for the rest of the file. |
| 108 | let step = 8usize.saturating_add(len).saturating_add(len & 1); |
| 109 | if step == 0 || step > at.len() { |
| 110 | break; |
| 111 | } |
| 112 | at = &at[step..]; |
| 113 | } |
| 114 | Ok(()) |
| 115 | } |
| 116 | |
| 117 | /// The main header: the size of the picture and how long a frame lasts. |
| 118 | fn read_avih(&mut self, body: &[u8]) { |
| 119 | if body.len() < 40 { |
| 120 | return; |
| 121 | } |
| 122 | self.micros = u32_at(body, 0); |
| 123 | self.frames = u32_at(body, 16); |
| 124 | self.w = u32_at(body, 32); |
| 125 | self.h = u32_at(body, 36); |
| 126 | } |
| 127 | |
| 128 | /// A stream header, taken only where it is the video stream. |
| 129 | /// |
| 130 | /// The first video stream wins. A file with two is a file with an |
| 131 | /// alternative take in it, and the catalogue wants the one it will show. |
| 132 | fn read_strh(&mut self, body: &[u8]) { |
| 133 | if body.len() < 36 || &body[..4] != VIDEO || self.rate != 0 { |
| 134 | return; |
| 135 | } |
| 136 | self.codec.copy_from_slice(&body[4..8]); |
| 137 | self.scale = u32_at(body, 20); |
| 138 | self.rate = u32_at(body, 24); |
| 139 | self.length = u32_at(body, 32); |
| 140 | } |
| 141 | |
| 142 | pub fn size(&self) -> (u32, u32) { |
| 143 | (self.w, self.h) |
| 144 | } |
| 145 | |
| 146 | /// `MJPG` and `dvsd` are what a camera of this era writes. The code is not |
| 147 | /// interpreted here; a caller deciding whether it can draw a frame is the |
| 148 | /// one that knows. |
| 149 | pub fn codec(&self) -> [u8; 4] { |
| 150 | self.codec |
| 151 | } |
| 152 | |
| 153 | /// The stream's own header is preferred over the main one. The main header's |
| 154 | /// frame count is a single 32-bit field written before the file was |
| 155 | /// finished, and the OpenDML extensions leave it nought on a file that grew |
| 156 | /// past four gigabytes; the stream header's length is the one a player |
| 157 | /// trusts. The main header stands in only where there is no video stream |
| 158 | /// header to consult. |
| 159 | pub fn millis(&self) -> Option<u64> { |
| 160 | if self.rate > 0 && self.scale > 0 && self.length > 0 { |
| 161 | // length * scale / rate is the running time in seconds, computed in |
| 162 | // 64 bits because length times scale overflows 32 readily. |
| 163 | let ms = (self.length as u64) |
| 164 | .saturating_mul(self.scale as u64) |
| 165 | .saturating_mul(1000) |
| 166 | / (self.rate as u64); |
| 167 | return Some(ms); |
| 168 | } |
| 169 | if self.micros > 0 && self.frames > 0 { |
| 170 | return Some((self.frames as u64).saturating_mul(self.micros as u64) / 1000); |
| 171 | } |
| 172 | None |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | /// Does a head begin a RIFF file whose form type is `AVI `? |
| 177 | /// |
| 178 | /// The trailing space is part of the code. `RIFF....WEBP` is the other RIFF form |
| 179 | /// a photo library meets, so the form type is what tells them apart and the |
| 180 | /// leading `RIFF` on its own is not enough. |
| 181 | pub fn is_avi(head: &[u8]) -> bool { |
| 182 | head.len() >= 12 && &head[..4] == b"RIFF" && &head[8..12] == b"AVI " |
| 183 | } |
| 184 | |
| 185 | /// A little-endian 32-bit value, nought where the buffer is too short. |
| 186 | fn u32_at(b: &[u8], at: usize) -> u32 { |
| 187 | if b.len() < at + 4 { |
| 188 | return 0; |
| 189 | } |
| 190 | u32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]]) |
| 191 | } |
| 192 | |
| 193 | #[cfg(test)] |
| 194 | mod tests { |
| 195 | use super::*; |
| 196 | |
| 197 | /// Builds a chunk: a code, a little-endian length, the body, and the pad |
| 198 | /// byte an odd length requires. |
| 199 | fn chunk(id: &[u8; 4], body: &[u8]) -> Vec<u8> { |
| 200 | let mut out = Vec::new(); |
| 201 | out.extend_from_slice(id); |
| 202 | out.extend_from_slice(&(body.len() as u32).to_le_bytes()); |
| 203 | out.extend_from_slice(body); |
| 204 | if body.len() & 1 == 1 { |
| 205 | out.push(0); |
| 206 | } |
| 207 | out |
| 208 | } |
| 209 | |
| 210 | fn avih(micros: u32, frames: u32, w: u32, h: u32) -> Vec<u8> { |
| 211 | let mut b = vec![0u8; 56]; |
| 212 | b[0..4].copy_from_slice(µs.to_le_bytes()); |
| 213 | b[16..20].copy_from_slice(&frames.to_le_bytes()); |
| 214 | b[32..36].copy_from_slice(&w.to_le_bytes()); |
| 215 | b[36..40].copy_from_slice(&h.to_le_bytes()); |
| 216 | b |
| 217 | } |
| 218 | |
| 219 | fn strh(kind: &[u8; 4], codec: &[u8; 4], scale: u32, rate: u32, length: u32) -> Vec<u8> { |
| 220 | let mut b = vec![0u8; 56]; |
| 221 | b[0..4].copy_from_slice(kind); |
| 222 | b[4..8].copy_from_slice(codec); |
| 223 | b[20..24].copy_from_slice(&scale.to_le_bytes()); |
| 224 | b[24..28].copy_from_slice(&rate.to_le_bytes()); |
| 225 | b[32..36].copy_from_slice(&length.to_le_bytes()); |
| 226 | b |
| 227 | } |
| 228 | |
| 229 | fn file(inner: Vec<u8>) -> Vec<u8> { |
| 230 | let mut out = Vec::new(); |
| 231 | out.extend_from_slice(b"RIFF"); |
| 232 | out.extend_from_slice(&((inner.len() + 4) as u32).to_le_bytes()); |
| 233 | out.extend_from_slice(b"AVI "); |
| 234 | out.extend_from_slice(&inner); |
| 235 | out |
| 236 | } |
| 237 | |
| 238 | /// A header list as a camera writes one: `hdrl` holding `avih` and a |
| 239 | /// `LIST strl` whose first chunk is `strh`. |
| 240 | fn hdrl(main: Vec<u8>, stream: Vec<u8>) -> Vec<u8> { |
| 241 | let mut strl = b"strl".to_vec(); |
| 242 | strl.extend_from_slice(&chunk(b"strh", &stream)); |
| 243 | let mut body = b"hdrl".to_vec(); |
| 244 | body.extend_from_slice(&chunk(b"avih", &main)); |
| 245 | body.extend_from_slice(&chunk(b"LIST", &strl)); |
| 246 | chunk(b"LIST", &body) |
| 247 | } |
| 248 | |
| 249 | #[test] |
| 250 | fn the_size_and_running_time_are_read() -> Outcome<()> { |
| 251 | // Ten seconds at 25 frames a second, 640 by 480. |
| 252 | let bytes = file(hdrl( |
| 253 | avih(40_000, 250, 640, 480), |
| 254 | strh(b"vids", b"MJPG", 1, 25, 250), |
| 255 | )); |
| 256 | let avi = res!(Avi::read(&bytes)); |
| 257 | req!(avi.size(), (640u32, 480u32)); |
| 258 | req!(avi.millis(), Some(10_000u64)); |
| 259 | req!(&avi.codec(), b"MJPG"); |
| 260 | Ok(()) |
| 261 | } |
| 262 | |
| 263 | #[test] |
| 264 | fn the_stream_header_is_preferred_to_the_main_one() -> Outcome<()> { |
| 265 | // What an OpenDML file looks like: the main header's frame count was |
| 266 | // never filled in, and only the stream knows the length. |
| 267 | let bytes = file(hdrl( |
| 268 | avih(40_000, 0, 720, 576), |
| 269 | strh(b"vids", b"dvsd", 1, 25, 500), |
| 270 | )); |
| 271 | let avi = res!(Avi::read(&bytes)); |
| 272 | req!(avi.millis(), Some(20_000u64), |
| 273 | "The main header's nought frames were used in place of the stream's."); |
| 274 | Ok(()) |
| 275 | } |
| 276 | |
| 277 | #[test] |
| 278 | fn an_audio_stream_is_not_mistaken_for_the_picture() -> Outcome<()> { |
| 279 | // `auds` first, and its rate and scale are nothing to do with frames. |
| 280 | let mut strl_a = b"strl".to_vec(); |
| 281 | strl_a.extend_from_slice(&chunk(b"strh", &strh(b"auds", b"\0\0\0\0", 1, 44_100, 441_000))); |
| 282 | let mut body = b"hdrl".to_vec(); |
| 283 | body.extend_from_slice(&chunk(b"avih", &avih(40_000, 250, 640, 480))); |
| 284 | body.extend_from_slice(&chunk(b"LIST", &strl_a)); |
| 285 | let mut strl_v = b"strl".to_vec(); |
| 286 | strl_v.extend_from_slice(&chunk(b"strh", &strh(b"vids", b"MJPG", 1, 25, 250))); |
| 287 | body.extend_from_slice(&chunk(b"LIST", &strl_v)); |
| 288 | let bytes = file(chunk(b"LIST", &body)); |
| 289 | |
| 290 | let avi = res!(Avi::read(&bytes)); |
| 291 | req!(avi.millis(), Some(10_000u64), |
| 292 | "The audio stream's rate was read as a frame rate."); |
| 293 | req!(&avi.codec(), b"MJPG"); |
| 294 | Ok(()) |
| 295 | } |
| 296 | |
| 297 | #[test] |
| 298 | fn a_head_answers_as_well_as_the_whole_file() -> Outcome<()> { |
| 299 | // The film's data follows the header list and is not in hand. The walk |
| 300 | // must answer from what it has rather than refusing. |
| 301 | let mut bytes = file(hdrl( |
| 302 | avih(33_333, 300, 1280, 720), |
| 303 | strh(b"vids", b"MJPG", 1, 30, 300), |
| 304 | )); |
| 305 | bytes.extend_from_slice(&chunk(b"LIST", b"movi")); |
| 306 | // Claim a great deal more `movi` than is present, as a real file does |
| 307 | // once only its front has been read. |
| 308 | let n = bytes.len(); |
| 309 | bytes[n - 8..n - 4].copy_from_slice(b"LIST"); |
| 310 | let avi = res!(Avi::read(&bytes)); |
| 311 | req!(avi.size(), (1280u32, 720u32)); |
| 312 | Ok(()) |
| 313 | } |
| 314 | |
| 315 | #[test] |
| 316 | fn a_webp_is_not_an_avi() -> Outcome<()> { |
| 317 | let mut bytes = b"RIFF".to_vec(); |
| 318 | bytes.extend_from_slice(&64u32.to_le_bytes()); |
| 319 | bytes.extend_from_slice(b"WEBPVP8 "); |
| 320 | req!(is_avi(&bytes), false); |
| 321 | req!(Avi::read(&bytes).is_err(), true, |
| 322 | "A WebP was read as a film."); |
| 323 | Ok(()) |
| 324 | } |
| 325 | } |