Skip to content

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 →