Skip to content

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

// Media Metadata Inspector — read container metadata straight from the bytes.
//
// Language: Swift 5.9+ (Foundation only)
// 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; nil only for inputs too short for a signature.
//
// Endianness trap: MP4 and ID3 fields are big-endian, while RIFF/WAVE fields
// and GIF dimensions are little-endian — the readers below are named for the
// byte order they produce, because mixing them up is the classic bug here.
// Swift's shift operators also require the shift amount to fit the left-hand
// type, so byte assembly goes through UInt32/UInt16 widening first.

import Foundation

// MARK: - Types

enum MediaKind: String {
    case mp4, mp3, wav, gif, webm, unknown
}

/// Container metadata; nil optionals mean "container parsed, field absent".
struct MediaMeta {
    var kind = MediaKind.unknown
    var durationSec: Double? = nil
    var width: Double? = nil
    var height: Double? = nil
    var title: String? = nil
    var artist: String? = nil
    var album: String? = nil

    init(kind: MediaKind) {
        self.kind = kind
    }
}

// MARK: - Byte readers

/// 4 ASCII bytes at `start` as a String (box types, chunk ids, frame ids).
func ascii(_ b: [UInt8], _ start: Int, _ len: Int) -> String {
    String(decoding: b[start..<(start + len)], as: UTF8.self)
}

/// Big-endian u32 — MP4 box headers, ID3 frame sizes, mvhd/tkhd fields.
func u32be(_ b: [UInt8], _ o: Int) -> UInt32 {
    (UInt32(b[o]) << 24) | (UInt32(b[o + 1]) << 16)
        | (UInt32(b[o + 2]) << 8) | UInt32(b[o + 3])
}

/// Little-endian u32 — RIFF chunk sizes, WAV byteRate.
func u32le(_ b: [UInt8], _ o: Int) -> UInt32 {
    UInt32(b[o]) | (UInt32(b[o + 1]) << 8)
        | (UInt32(b[o + 2]) << 16) | (UInt32(b[o + 3]) << 24)
}

/// Little-endian u16 — GIF canvas width/height.
func u16le(_ b: [UInt8], _ o: Int) -> UInt16 {
    UInt16(b[o]) | (UInt16(b[o + 1]) << 8)
}

/// ID3 syncsafe integer: four 7-bit groups, so the high bit of every byte is
/// free to keep MPEG audio frame syncs unambiguous.
func syncsafe(_ b: [UInt8], _ o: Int) -> Int {
    (Int(b[o]) << 21) | (Int(b[o + 1]) << 14) | (Int(b[o + 2]) << 7) | Int(b[o + 3])
}

// MARK: - MP4

/// One MP4 box: its type and the half-open range [start, end) of its payload.
struct Box {
    let type: String
    let start: Int
    let end: Int
}

/// MP4 box walk: every box of a wanted type between `start` and `end`.
func findBoxes(_ bytes: [UInt8], _ start: Int, _ end: Int, _ wanted: [String]) -> [Box] {
    var found: [Box] = []
    var pos = start
    while pos + 8 <= end {
        var size = Int(u32be(bytes, pos))
        if size == 1 { return found } // 64-bit sizes unsupported — stop safely.
        if size == 0 { size = end - pos } // size 0: box extends to the end
        if size < 8 || pos + size > end { return found }
        let type = ascii(bytes, pos + 4, 4)
        if wanted.contains(type) {
            found.append(Box(type: type, start: pos + 8, end: pos + size))
        }
        pos += size
    }
    return found
}

/// MP4: moov -> mvhd gives duration; the first trak's tkhd gives dimensions.
func probeMp4(_ bytes: [UInt8]) -> MediaMeta {
    var meta = MediaMeta(kind: .mp4)
    for moov in findBoxes(bytes, 0, bytes.count, ["moov"]) {
        for mvhd in findBoxes(bytes, moov.start, moov.end, ["mvhd"]) {
            let version = bytes[mvhd.start]
            if version == 0, mvhd.end - mvhd.start >= 20 {
                let timescale = u32be(bytes, mvhd.start + 12)
                if timescale > 0 {
                    meta.durationSec = Double(u32be(bytes, mvhd.start + 16)) / Double(timescale)
                }
            } else if version == 1, mvhd.end - mvhd.start >= 32 {
                let timescale = u32be(bytes, mvhd.start + 20)
                // 64-bit duration: high and low halves of the u64, each u32.
                let duration = Double(u32be(bytes, mvhd.start + 24)) * 0x1_0000_0000
                    + Double(u32be(bytes, mvhd.start + 28))
                if timescale > 0 {
                    meta.durationSec = duration / Double(timescale)
                }
            }
        }
        for trak in findBoxes(bytes, moov.start, moov.end, ["trak"]) {
            guard let tkhd = findBoxes(bytes, trak.start, trak.end, ["tkhd"]).first,
                  bytes[tkhd.start] == 0, tkhd.end - tkhd.start >= 84 else { continue }
            // tkhd stores width/height as 16.16 fixed point — divide by 65536.
            meta.width = Double(u32be(bytes, tkhd.start + 76)) / 65536
            meta.height = Double(u32be(bytes, tkhd.start + 80)) / 65536
            break // first track is enough
        }
    }
    return meta
}

// MARK: - ID3v2

/// Decode one ID3 text frame (first byte selects the encoding).
func decodeId3Text(_ bytes: [UInt8], _ start: Int, _ len: Int) -> String {
    if len <= 0 { return "" }
    let encoding = bytes[start]
    let text = Array(bytes[(start + 1)..<(start + len)])
    if encoding == 3 {
        // UTF-8 (ID3v2.4); stop at the null terminator.
        let end = text.firstIndex(of: 0) ?? text.count
        return String(decoding: text[0..<end], as: UTF8.self)
    }
    if encoding == 1 || encoding == 2 {
        // UTF-16: encoding 1 carries a BOM (sniff endianness); 2 is BOM-less BE.
        let bom = encoding == 1 && (text.first == 0xff || text.first == 0xfe)
        let littleEndian = bom ? text[0] == 0xff : false
        var units: [UInt16] = []
        var i = bom ? 2 : 0
        while i + 1 < text.count {
            let code: UInt16 = littleEndian
                ? UInt16(text[i]) | (UInt16(text[i + 1]) << 8)
                : (UInt16(text[i]) << 8) | UInt16(text[i + 1])
            if code == 0 { break }
            units.append(code)
            i += 2
        }
        // String(decoding:as:) mirrors TextDecoder(): unpaired surrogates
        // become U+FFFD instead of trapping.
        return String(decoding: units, as: UTF16.self)
    }
    var out = ""
    for byte in text {
        if byte == 0 { break }
        out.append(Character(UnicodeScalar(byte))) // ISO-8859-1 / ASCII range
    }
    return out
}

/// ID3v2: walk frames, keep TIT2 (title), TPE1 (artist), TALB (album).
func probeId3(_ bytes: [UInt8]) -> MediaMeta {
    var meta = MediaMeta(kind: .mp3)
    let version = bytes[3]
    let tagSize = syncsafe(bytes, 6)
    var pos = 10
    let end = min(10 + tagSize, bytes.count)
    while pos + 10 <= end {
        let id = ascii(bytes, pos, 4)
        guard id.range(of: "^[A-Z0-9]{4}$", options: .regularExpression) != nil else { break }
        // ID3v2.4 frame sizes are syncsafe; v2.2/v2.3 are plain big-endian.
        let size = version == 4 ? syncsafe(bytes, pos + 4) : Int(u32be(bytes, pos + 4))
        if size == 0 { break }
        // Clamp declared size to the tag end: a corrupt frame stays inside the tag.
        let avail = max(end - (pos + 10), 0)
        let text = decodeId3Text(bytes, pos + 10, min(size, avail))
        switch id {
        case "TIT2": meta.title = text
        case "TPE1": meta.artist = text
        case "TALB": meta.album = text
        default: break
        }
        pos += 10 + size
    }
    return meta
}

// MARK: - WAV

/// WAV: fmt chunk gives byteRate; data chunk size / byteRate = duration.
func probeWav(_ bytes: [UInt8]) -> MediaMeta {
    var meta = MediaMeta(kind: .wav)
    var pos = 12 // past RIFF size + WAVE
    var byteRate: UInt32 = 0
    while pos + 8 <= bytes.count {
        let id = ascii(bytes, pos, 4)
        let size = u32le(bytes, pos + 4)
        if id == "fmt " && pos + 8 + 16 <= bytes.count {
            byteRate = u32le(bytes, pos + 16) // byteRate sits at fmt payload offset 8
        } else if id == "data" && byteRate > 0 {
            meta.durationSec = Double(size) / Double(byteRate)
            break
        }
        pos += 8 + Int(size) + Int(size % 2) // chunks are word-aligned
    }
    return meta
}

// MARK: - Probe

/// Sniff the container and parse its metadata. nil when input is under 12
/// bytes — too short for any signature.
func probe(_ bytes: [UInt8]) -> MediaMeta? {
    if bytes.count < 12 { return nil }

    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" {
        var meta = MediaMeta(kind: .gif)
        meta.width = Double(u16le(bytes, 6))
        meta.height = Double(u16le(bytes, 8))
        return meta
    }
    if bytes[0] == 0x1a && bytes[1] == 0x45 && bytes[2] == 0xdf && bytes[3] == 0xa3 {
        return MediaMeta(kind: .webm) // EBML magic
    }
    return MediaMeta(kind: .unknown)
}

// Usage:
//   let meta = probe([UInt8](fileData))
//   print(meta?.kind.rawValue ?? "nil", meta?.durationSec ?? "-", meta?.width ?? "-")

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 →