Skip to content

Media Metadata Inspector — C# 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 C# implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Media Metadata Inspector — read container metadata straight from the bytes.
//
// Language: C# (.NET 8), 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
// tags. Deterministic; null only for inputs too short for a signature.

using System;
using System.Collections.Generic;
using System.Text;

namespace MediaMeta;

public enum MediaKind { Mp4, Mp3, Wav, Gif, Webm, Unknown }

/// <summary>Container metadata; nullable fields mean "not found".</summary>
public sealed class MediaMeta
{
    public required MediaKind Kind { get; init; }
    public double? DurationSec { get; init; }
    public int? Width { get; init; }
    public int? Height { get; init; }
    public string? Title { get; init; }
    public string? Artist { get; init; }
    public string? Album { get; init; }
}

public static class MediaMetadataInspector
{
    private static string Ascii(byte[] b, int start, int len)
    {
        // Trap: use Latin-1 semantics so bytes 0x80-0xFF survive round-trips.
        return Encoding.Latin1.GetString(b, start, len);
    }

    /// <summary>Big-endian 16-bit (MP4, GIF).</summary>
    private static ushort U16(byte[] b, int o) => (ushort)((b[o] << 8) | b[o + 1]);

    /// <summary>Big-endian 32-bit (MP4, ID3).</summary>
    private static uint U32(byte[] b, int o) =>
        (uint)((b[o] << 24) | (b[o + 1] << 16) | (b[o + 2] << 8) | b[o + 3]);

    /// <summary>Little-endian 32-bit — RIFF/WAV fields, unlike MP4/ID3.</summary>
    private static uint U32Le(byte[] b, int o) =>
        (uint)(b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24));

    private readonly record struct BoxRange(string Type, int Start, int End);

    /// <summary>MP4 box walk: returns payload offsets for every box of the wanted types.</summary>
    private static List<BoxRange> FindBoxes(byte[] bytes, int start, int end, string[] wanted)
    {
        var found = new List<BoxRange>();
        int pos = start;
        while (pos + 8 <= end)
        {
            uint size = U32(bytes, pos);
            const int headerSize = 8;
            if (size == 1) return found;       // 64-bit sizes unsupported — stop safely.
            if (size == 0) size = (uint)(end - pos); // box extends to the end
            if (size < headerSize || pos + size > end) return found;
            string type = Ascii(bytes, pos + 4, 4);
            if (Array.IndexOf(wanted, type) >= 0)
                found.Add(new BoxRange(type, pos + headerSize, pos + (int)size));
            pos += (int)size;
        }
        return found;
    }

    /// <summary>MP4: moov -&gt; mvhd gives duration; the first trak's tkhd gives dimensions.</summary>
    private static MediaMeta ProbeMp4(byte[] bytes)
    {
        double? duration = null;
        int? width = null, height = null;

        foreach (var moov in FindBoxes(bytes, 0, bytes.Length, new[] { "moov" }))
        {
            foreach (var mvhd in FindBoxes(bytes, moov.Start, moov.End, new[] { "mvhd" }))
            {
                int version = bytes[mvhd.Start];
                if (version == 0 && mvhd.End - mvhd.Start >= 20)
                {
                    uint timescale = U32(bytes, mvhd.Start + 12);
                    if (timescale > 0)
                        // Trap: uint / uint truncates — cast to double like the TS port.
                        duration = U32(bytes, mvhd.Start + 16) / (double)timescale;
                }
                else if (version == 1 && mvhd.End - mvhd.Start >= 32)
                {
                    uint timescale = U32(bytes, mvhd.Start + 20);
                    // 64-bit duration assembled from two 32-bit halves (big-endian).
                    double d = U32(bytes, mvhd.Start + 24) * 4294967296.0 + U32(bytes, mvhd.Start + 28);
                    if (timescale > 0) duration = d / timescale;
                }
            }

            foreach (var trak in FindBoxes(bytes, moov.Start, moov.End, new[] { "trak" }))
            {
                var tkhds = FindBoxes(bytes, trak.Start, trak.End, new[] { "tkhd" });
                if (tkhds.Count == 0) continue;
                var tkhd = tkhds[0];
                if (bytes[tkhd.Start] == 0 && tkhd.End - tkhd.Start >= 84)
                {
                    // Width/height are 16.16 fixed-point in the visual presentation box.
                    width = (int)(U32(bytes, tkhd.Start + 76) / 65536);
                    height = (int)(U32(bytes, tkhd.Start + 80) / 65536);
                    break; // first track is enough
                }
            }
        }

        return new MediaMeta { Kind = MediaKind.Mp4, DurationSec = duration, Width = width, Height = height };
    }

    /// <summary>ID3v2 sync-safe integer: 7 bits per byte.</summary>
    private static int Syncsafe(byte[] b, int o) =>
        (b[o] << 21) | (b[o + 1] << 14) | (b[o + 2] << 7) | b[o + 3];

    /// <summary>Decode one ID3 text frame (first byte selects the encoding).</summary>
    public static string DecodeId3Text(byte[] bytes, int start, int len)
    {
        if (len <= 0) return "";
        int encoding = bytes[start];
        // Payload runs from start+1; stop early if the frame is truncated.
        int payloadLen = Math.Min(len - 1, bytes.Length - (start + 1));
        if (payloadLen <= 0) return "";

        if (encoding == 3)
        {
            // UTF-8 (ID3v2.4); stop at the null terminator.
            int zero = Array.IndexOf(bytes, (byte)0, start + 1, payloadLen);
            int textLen = zero == -1 ? payloadLen : zero - (start + 1);
            return Encoding.UTF8.GetString(bytes, start + 1, textLen);
        }
        if (encoding == 1 || encoding == 2)
        {
            // UTF-16: encoding 1 carries a BOM (sniff endianness); 2 is BOM-less BE.
            int o = start + 1;
            bool bom = encoding == 1 && (bytes[o] == 0xff || bytes[o] == 0xfe);
            int s = bom ? 2 : 0;
            bool littleEndian = bom && bytes[o] == 0xff;
            var sb = new StringBuilder();
            for (int i = s; i + 1 < payloadLen; i += 2)
            {
                int code = littleEndian
                    ? bytes[o + i] | (bytes[o + i + 1] << 8)
                    : (bytes[o + i] << 8) | bytes[o + i + 1];
                if (code == 0) break; // null terminator
                sb.Append((char)code);
            }
            return sb.ToString();
        }
        // ISO-8859-1 / ASCII range: Latin-1 mapping preserves every byte.
        var latin = new StringBuilder();
        for (int i = 0; i < payloadLen; i++)
        {
            if (bytes[start + 1 + i] == 0) break;
            latin.Append((char)bytes[start + 1 + i]);
        }
        return latin.ToString();
    }

    /// <summary>ID3v2: walk frames, keep TIT2 (title), TPE1 (artist), TALB (album).</summary>
    private static MediaMeta ProbeId3(byte[] bytes)
    {
        string? title = null, artist = null, album = null;
        int version = bytes[3];
        int tagSize = Syncsafe(bytes, 6);
        int pos = 10;
        int end = Math.Min(10 + tagSize, bytes.Length);

        while (pos + 10 <= end)
        {
            string id = Ascii(bytes, pos, 4);
            bool valid = id.Length == 4;
            foreach (char c in id) valid &= (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
            if (!valid) break;

            // v2.4 frame sizes are sync-safe; v2.3 are plain 32-bit BE.
            int size = version == 4 ? Syncsafe(bytes, pos + 4) : unchecked((int)U32(bytes, pos + 4));
            if (size == 0) break;

            // Clamp declared size to the tag end: a corrupt frame stays inside the tag.
            int avail = Math.Max(end - (pos + 10), 0);
            string text = DecodeId3Text(bytes, pos + 10, Math.Min(size, avail));
            if (id == "TIT2") title = text;
            else if (id == "TPE1") artist = text;
            else if (id == "TALB") album = text;
            pos += 10 + size;
        }

        return new MediaMeta { Kind = MediaKind.Mp3, Title = title, Artist = artist, Album = album };
    }

    /// <summary>WAV: fmt chunk gives byteRate; data chunk size / byteRate = duration.</summary>
    private static MediaMeta ProbeWav(byte[] bytes)
    {
        double? duration = null;
        int pos = 12; // past RIFF size + WAVE
        uint byteRate = 0;

        while (pos + 8 <= bytes.Length)
        {
            string id = Ascii(bytes, pos, 4);
            uint size = U32Le(bytes, pos + 4);
            if (id == "fmt " && pos + 8 + 16 <= bytes.Length)
            {
                byteRate = U32Le(bytes, pos + 16); // byteRate sits at fmt payload offset 8
            }
            else if (id == "data" && byteRate > 0)
            {
                duration = size / (double)byteRate;
                break;
            }
            pos += 8 + (int)size + (int)(size % 2); // chunks are word-aligned
        }

        return new MediaMeta { Kind = MediaKind.Wav, DurationSec = duration };
    }

    /// <summary>Sniff the container and parse its metadata. Null when input is under 12 bytes.</summary>
    public static MediaMeta? Probe(byte[] bytes)
    {
        if (bytes.Length < 12) return null;

        if (Ascii(bytes, 4, 4) == "ftyp") return ProbeMp4(bytes); // MP4 / MOV
        if (Ascii(bytes, 0, 3) == "ID3") return ProbeId3(bytes);
        if (Ascii(bytes, 0, 4) == "RIFF" && Ascii(bytes, 8, 4) == "WAVE") return ProbeWav(bytes);
        if (Ascii(bytes, 0, 4) == "GIF8")
            return new MediaMeta { Kind = MediaKind.Gif, Width = U16(bytes, 6), Height = U16(bytes, 8) };
        if (bytes[0] == 0x1a && bytes[1] == 0x45 && bytes[2] == 0xdf && bytes[3] == 0xa3)
            return new MediaMeta { Kind = MediaKind.Webm }; // EBML magic
        return new MediaMeta { Kind = MediaKind.Unknown };
    }
}

// Usage:
//   var meta = MediaMetadataInspector.Probe(File.ReadAllBytes(path));
//   Console.WriteLine($"{meta?.Kind} {meta?.DurationSec} {meta?.Width}x{meta?.Height}");

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 →