mcap/modules/Image_Decode/video.jai

112 lines
5.0 KiB
Plaintext

// 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 == .LINUX {
video_poster :: (path: string) -> Image, Video_Info, ok: bool, error: string {
return .{}, .{}, false, "videos are only read on Windows and macOS";
}
}
// 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;
}