Oregami
Repositories/oxedyne/fe2o3

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
37use oxedyne_fe2o3_core::prelude::*;
38
39const 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.
44const CHUNK_LIMIT: usize = 64;
45
46/// What a film's header says about it.
47#[derive(Clone, Debug, Default, Eq, PartialEq)]
48pub 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
60impl 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.
181pub 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.
186fn 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)]
194mod 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(&micros.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}