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 -> 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 →