Media Metadata Inspector — Zig source
Inspect audio, video and image files at the byte level — MP4 boxes, ID3 tags, WAV chunks, GIF headers — parsed by our own readers, entirely in your browser.
This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Media Metadata Inspector — read container metadata straight from the bytes.
//
// Language: Zig (0.13+), standard library only
// CosmoDev polyglot showcase port of the `media-metadata-inspector` tool.
// Ported from src/lib/media-meta.ts — display source, part of CosmoDev's
// polyglot tool pages.
//
// Sniffs the container signature (MP4 "ftyp" box, "ID3", RIFF/WAVE, GIF8,
// WebM EBML magic), then walks that container for duration, dimensions, and
// ID3 tags. Deterministic; probe() returns kind .unknown for inputs too
// short for a signature — error only on caller misuse, never on bad input.
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const Kind = enum { mp4, mp3, wav, gif, webm, unknown };
pub const MediaMeta = struct {
kind: Kind = .unknown,
duration_sec: ?f64 = null,
width: ?f64 = null,
height: ?f64 = null,
title: ?[]const u8 = null,
artist: ?[]const u8 = null,
album: ?[]const u8 = null,
/// Strings are allocated from the caller's arena — free via this or use
/// an arena allocator around one probe call.
pub fn deinit(self: *MediaMeta, gpa: Allocator) void {
if (self.title) |s| gpa.free(s);
if (self.artist) |s| gpa.free(s);
if (self.album) |s| gpa.free(s);
}
};
const Box = struct { start: usize, end: usize };
fn u16be(b: []const u8, o: usize) u16 {
return (@as(u16, b[o]) << 8) | b[o + 1];
}
fn u16le(b: []const u8, o: usize) u16 {
return @as(u16, b[o]) | (@as(u16, b[o + 1]) << 8);
}
fn u32be(b: []const u8, o: usize) u32 {
return (@as(u32, b[o]) << 24) | (@as(u32, b[o + 1]) << 16) |
(@as(u32, b[o + 2]) << 8) | b[o + 3];
}
fn u32le(b: []const u8, o: usize) u32 {
return @as(u32, b[o]) | (@as(u32, b[o + 1]) << 8) |
(@as(u32, b[o + 2]) << 16) | (@as(u32, b[o + 3]) << 24);
}
fn is4(b: []const u8, o: usize, comptime tag: *const [4]u8) bool {
return o + 4 <= b.len and std.mem.eql(u8, b[o .. o + 4], tag);
}
/// MP4 box walk: payload ranges for every box of the wanted type.
/// 64-bit sizes (size==1) are unsupported — stop safely, like the TS lib.
fn findBoxes(b: []const u8, from: usize, end: usize, comptime want: *const [4]u8, out: *std.ArrayList(Box)) !void {
var pos = from;
while (pos + 8 <= end) {
var size = u32be(b, pos);
if (size == 1) return;
if (size == 0) size = @intCast(end - pos); // extends to the end
if (size < 8 or pos + size > end) return;
if (is4(b, pos + 4, want)) {
try out.append(.{ .start = pos + 8, .end = pos + size });
}
pos += size;
}
}
fn probeMp4(gpa: Allocator, b: []const u8, m: *MediaMeta) !void {
m.kind = .mp4;
var moovs = std.ArrayList(Box).init(gpa);
defer moovs.deinit();
try findBoxes(b, 0, b.len, "moov", &moovs);
for (moovs.items) |moov| {
var mvhds = std.ArrayList(Box).init(gpa);
defer mvhds.deinit();
try findBoxes(b, moov.start, moov.end, "mvhd", &mvhds);
for (mvhds.items) |mvhd| {
const version = b[mvhd.start];
if (version == 0 and mvhd.end - mvhd.start >= 20) {
const timescale = u32be(b, mvhd.start + 12);
const duration = u32be(b, mvhd.start + 16);
if (timescale > 0)
m.duration_sec = @as(f64, @floatFromInt(duration)) / @as(f64, @floatFromInt(timescale));
} else if (version == 1 and mvhd.end - mvhd.start >= 32) {
const timescale = u32be(b, mvhd.start + 20);
// 64-bit duration as two 32-bit halves — f64 holds it exactly.
const duration = @as(f64, @floatFromInt(u32be(b, mvhd.start + 24))) * 4294967296.0 +
@as(f64, @floatFromInt(u32be(b, mvhd.start + 28)));
if (timescale > 0) m.duration_sec = duration / @as(f64, @floatFromInt(timescale));
}
}
var traks = std.ArrayList(Box).init(gpa);
defer traks.deinit();
try findBoxes(b, moov.start, moov.end, "trak", &traks);
for (traks.items) |trak| {
var tkhds = std.ArrayList(Box).init(gpa);
defer tkhds.deinit();
try findBoxes(b, trak.start, trak.end, "tkhd", &tkhds);
if (tkhds.items.len > 0 and b[tkhds.items[0].start] == 0 and
tkhds.items[0].end - tkhds.items[0].start >= 84)
{
// Width/height ride in the high 16 bits of fixed-point 16.16.
m.width = @as(f64, @floatFromInt(u32be(b, tkhds.items[0].start + 76))) / 65536.0;
m.height = @as(f64, @floatFromInt(u32be(b, tkhds.items[0].start + 80))) / 65536.0;
}
if (m.width != null) break; // first track is enough
}
}
}
fn syncsafe(b: []const u8, o: usize) u32 {
return (@as(u32, b[o] & 0x7F) << 21) | (@as(u32, b[o + 1] & 0x7F) << 14) |
(@as(u32, b[o + 2] & 0x7F) << 7) | @as(u32, b[o + 3] & 0x7F);
}
/// ID3 text frame: encoding byte (0 latin1, 3 utf-8), then the text. The
/// slice stops at the null terminator; UTF-16 encodings live in the TS
/// reference. Returned slice is a dup — caller owns it.
fn id3Text(gpa: Allocator, b: []const u8, start: usize, len: usize) ![]const u8 {
if (len <= 1) return "";
const text = b[start + 1 .. start + len];
const n = std.mem.indexOfScalar(u8, text, 0) orelse text.len;
return gpa.dupe(u8, text[0..n]);
}
fn probeId3(gpa: Allocator, b: []const u8, m: *MediaMeta) !void {
m.kind = .mp3;
const tag_size = syncsafe(b, 6);
const end = @min(10 + tag_size, @as(u32, @intCast(b.len)));
var pos: usize = 10;
while (pos + 10 <= end) {
const id = b[pos .. pos + 4];
if (id[0] == 0) break; // padding
const frame_size = syncsafe(b, pos + 4); // v2.4 syncsafe; v2.3 differs
if (frame_size == 0 or pos + 10 + frame_size > end) break;
const text = try id3Text(gpa, b, pos + 10, frame_size);
if (std.mem.eql(u8, id, "TIT2") and text.len > 0) m.title = text
else if (std.mem.eql(u8, id, "TPE1") and text.len > 0) m.artist = text
else if (std.mem.eql(u8, id, "TALB") and text.len > 0) m.album = text
else gpa.free(text);
pos += 10 + frame_size;
}
}
/// Probe a container byte stream. Strings inside the result are allocated —
/// call deinit on the result (or use an arena).
pub fn probe(gpa: Allocator, bytes: []const u8) !MediaMeta {
var m = MediaMeta{};
if (bytes.len < 12) return m; // too short for ANY signature
if (is4(bytes, 4, "ftyp")) {
try probeMp4(gpa, bytes, &m);
} else if (std.mem.eql(u8, bytes[0..3], "ID3")) {
try probeId3(gpa, bytes, &m);
} else if (is4(bytes, 0, "RIFF") and is4(bytes, 8, "WAVE")) {
m.kind = .wav;
// PCM fmt chunk: byteRate at offset 28 → duration = data bytes / rate.
const byte_rate = u32le(bytes, 28);
if (byte_rate > 0 and bytes.len > 44)
m.duration_sec = @as(f64, @floatFromInt(bytes.len - 44)) / @as(f64, @floatFromInt(byte_rate));
} else if (std.mem.eql(u8, bytes[0..4], "GIF8")) {
m.kind = .gif;
m.width = @floatFromInt(u16le(bytes, 6)); // logical screen descriptor
m.height = @floatFromInt(u16le(bytes, 8));
} else if (std.mem.eql(u8, bytes[0..4], "\x1A\x45\xDF\xA3")) {
m.kind = .webm;
// Duration lives in a float SecsInfo element — the TS lib scans for
// it; this port flags the kind (documented gap).
}
return m;
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const gpa = arena.allocator();
// Smoke: a minimal MP4-sized buffer with an ftyp brand at offset 4.
var mp4 = [_]u8{0} ** 32;
@memcpy(mp4[4..12], "ftypisom");
const meta = try probe(gpa, &mp4);
std.debug.print("{s}\n", .{@tagName(meta.kind)}); // mp4
const tiny = try probe(gpa, &[_]u8{ 1, 2, 3 });
std.debug.print("{s}\n", .{@tagName(tiny.kind)}); // unknown
}
Also available in 9 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →