// Videos and animations: a video's poster frame and what its container says (FFmpeg, modules/ffmpeg, // on Windows), and how many frames an animated GIF has. A video is read from its file, a little at a // time, never all in memory. // What a video is, as video_poster finds it. The strings are temp. Video_Info :: struct { width, height: s32; // as shown: turned, and stretched by a non-square pixel shape duration: float64; // seconds; 0 when unknown frame_rate: float64; // frames a second; 0 when unknown codec: string; // "h264", "hevc", "vp9" ... rotation: s32; // degrees clockwise the frames are turned to be shown: 0, 90, 180 or 270 poster_time: float64; // seconds into the video of the poster frame has_audio: bool; // From the container, as written; "" when it has none. creation_date: string; // Apple's com.apple.quicktime.creationdate: local time and its offset, "2024-05-12T10:15:30+0100" creation_time: string; // creation_time: UTC, "2024-05-12T09:15:30.000000Z" make, model, software: string; location: string; // ISO 6709, "+51.5072-000.1276+011.000/"; see iso6709_parse } // The frame a video is shown as, turned the right way up, and what it is. The frame is a tenth of // the way in (at most VIDEO_POSTER_MAX_SECONDS), past the black or blurred first frames phones often // start with; the first frame of a clip shorter than a second. VIDEO_POSTER_MAX_SECONDS :: 3.0; #if OS != .WINDOWS { video_poster :: (path: string) -> Image, Video_Info, ok: bool, error: string { return .{}, .{}, false, "videos are only read on Windows"; } } // Degrees clockwise to turn a video's frames, from its display matrix (as ffmpeg's // av_display_rotation_get, whose angle is anticlockwise): 0, 90, 180 or 270. display_matrix_rotation :: (m: [9] s32) -> s32 { fixed :: (v: s32) -> float64 { return cast(float64) v / 65536.0; } scale0 := sqrt(fixed(m[0]) * fixed(m[0]) + fixed(m[3]) * fixed(m[3])); scale1 := sqrt(fixed(m[1]) * fixed(m[1]) + fixed(m[4]) * fixed(m[4])); if scale0 == 0 || scale1 == 0 return 0; anticlockwise := -atan2(fixed(m[1]) / scale1, fixed(m[0]) / scale0) * 180 / PI; clockwise := cast(s32) floor(-anticlockwise / 90 + 0.5) * 90; clockwise %= 360; if clockwise < 0 clockwise += 360; return clockwise; } // "+51.5072-000.1276+011.000/" (ISO 6709: latitude, longitude, then an altitude when there is one) // as numbers. ok is false when it is not that. iso6709_parse :: (text: string) -> latitude: float64, longitude: float64, altitude: float64, has_altitude: bool, ok: bool { values: [3] float64; count := 0; at := 0; while at < text.count && count < 3 { c := text[at]; if c == #char "/" break; if c != #char "+" && c != #char "-" return 0, 0, 0, false, false; end := at + 1; while end < text.count && (is_digit(text[end]) || text[end] == #char ".") end += 1; if end == at + 1 return 0, 0, 0, false, false; value, value_ok := string_to_float64(slice(text, at, end - at)); if !value_ok return 0, 0, 0, false, false; values[count] = value; count += 1; at = end; } if count < 2 || values[0] < -90 || values[0] > 90 || values[1] < -180 || values[1] > 180 return 0, 0, 0, false, false; return values[0], values[1], values[2], count == 3, true; } // How many frames a GIF has, and how long it runs in milliseconds, from its blocks; no pixels are // decoded. A frame with no delay, or 10 ms, shows for 100 ms, as browsers show it. ok is false // when the bytes are not a GIF; one cut short counts the frames up to where it ends. gif_info :: (bytes: [] u8) -> frames: s64, duration_ms: s64, ok: bool { if bytes.count < 13 || (memcmp(bytes.data, "GIF87a".data, 6) != 0 && memcmp(bytes.data, "GIF89a".data, 6) != 0) return 0, 0, false; at := 13; flags := bytes[10]; if flags & 0x80 at += 3 * (1 << ((flags & 7) + 1)); // the global colour table frames, duration := 0, 0; delay := 0; skip_blocks :: (bytes: [] u8, at: s64) -> s64 { // data sub-blocks, to the empty one that ends them while at < bytes.count { size := cast(s64) bytes[at]; at += 1 + size; if size == 0 break; } return at; } while at < bytes.count { if bytes[at] == { case 0x21; // an extension if at + 1 >= bytes.count break; if bytes[at + 1] == 0xF9 && at + 6 < bytes.count { // graphic control: the next frame's delay, in hundredths delay = (cast(s64) bytes[at + 4]) | ((cast(s64) bytes[at + 5]) << 8); } at = skip_blocks(bytes, at + 2); case 0x2C; // a frame if at + 10 > bytes.count break; local := bytes[at + 9]; at += 10; if local & 0x80 at += 3 * (1 << ((local & 7) + 1)); at = skip_blocks(bytes, at + 1); // after the LZW code size if at > bytes.count break; frames += 1; duration += ifx delay <= 1 then 100 else delay * 10; delay = 0; case; // 0x3B ends it; anything else is damage break; } } return frames, duration, true; }