- Import brings in videos (MP4, MOV, MKV, WebM, AVI, WMV, MPEG, MTS/M2TS, FLV) on Windows, read with FFmpeg: the file as it was in the original, a frame a tenth of the way in (at most 3 s), turned the right way up, as the rendered image (what the grid, Capture window, OCR and labels see), and import.json with Duration, FrameRate, VideoCodec, Rotation, PosterTime, Audio, and the camera, date and location from the container. Elsewhere videos are skipped as "video (Windows only)". - .mcap version 4: the original's size is a u64, so a video can be any size. Versions 1-3 still read. mcap_read_without_original and mcap_write_streamed read and rewrite a file with its original left on disk; mcap_store, mcap_erase, the import index and the import itself never hold a video in memory. - Animated GIFs: Frames and Duration in import.json (gif_info). - Grid: a play mark and duration on video thumbnails, GIF on animations. Capture window: what the video is, and Play in the default player (written out to %TEMP%\mcap). - modules/ffmpeg: FFmpeg 6.0 bindings and the five DLLs used (avformat, avcodec, avutil, swscale, swresample), copied beside the executable by the build. swscale converts into a buffer of its own allocator with padded rows: an exact-sized one let its SIMD write past the end, crashing on frames of odd widths (1916, 1106 ...). - image-probe shows videos and GIF frame counts; ocr-bench reads a video's frame. - Tests: new video formats, display-matrix rotation, ISO 6709, video dates, GIF frames, and v4 serialize/parse, v3 compatibility, streamed writes, store/erase and extraction. SnagitConverter's copy of Capture_File needs syncing to read version 4 files.
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 != .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;
|
|
}
|