112 lines
5.0 KiB
Plaintext
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;
|
|
}
|