Skip to content

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

// Media Metadata Inspector — read container metadata straight from the bytes.
//
// Language: Kotlin (1.9+), 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 — never throws.

import java.nio.charset.StandardCharsets

enum class Kind { MP4, MP3, WAV, GIF, WEBM, UNKNOWN }

data class MediaMeta(
    val kind: Kind = Kind.UNKNOWN,
    val durationSec: Double? = null,
    val width: Double? = null,
    val height: Double? = null,
    val title: String? = null,
    val artist: String? = null,
    val album: String? = null,
)

private class Builder {
    var kind: Kind = Kind.UNKNOWN
    var durationSec: Double? = null
    var width: Double? = null
    var height: Double? = null
    var title: String? = null
    var artist: String? = null
    var album: String? = null
    fun build() = MediaMeta(kind, durationSec, width, height, title, artist, album)
}

private class Box(val type: String, val start: Int, val end: Int)

private fun u16be(b: ByteArray, o: Int) = ((b[o].toInt() and 0xFF) shl 8) or (b[o + 1].toInt() and 0xFF)
private fun u16le(b: ByteArray, o: Int) = (b[o].toInt() and 0xFF) or ((b[o + 1].toInt() and 0xFF) shl 8)
private fun u32be(b: ByteArray, o: Int): Long =
    ((b[o].toLong() and 0xFF) shl 24) or ((b[o + 1].toLong() and 0xFF) shl 16) or
        ((b[o + 2].toLong() and 0xFF) shl 8) or (b[o + 3].toLong() and 0xFF)
private fun u32le(b: ByteArray, o: Int): Long =
    (b[o].toLong() and 0xFF) or ((b[o + 1].toLong() and 0xFF) shl 8) or
        ((b[o + 2].toLong() and 0xFF) shl 16) or ((b[o + 3].toLong() and 0xFF) shl 24)

private fun ascii(b: ByteArray, start: Int, len: Int) =
    String(b, start, len, StandardCharsets.ISO_8859_1)

/** MP4 box walk: payload ranges for every box of the wanted types. */
private fun findBoxes(b: ByteArray, from: Int, end: Int, vararg wanted: String): List<Box> {
    val found = mutableListOf<Box>()
    var pos = from
    while (pos + 8 <= end) {
        var size = u32be(b, pos)
        if (size == 1L) return found              // 64-bit sizes unsupported — stop safely
        if (size == 0L) size = (end - pos).toLong()  // extends to the end
        if (size < 8 || pos + size > end) return found
        val type = ascii(b, pos + 4, 4)
        if (type in wanted) found.add(Box(type, pos + 8, (pos + size).toInt()))
        pos += size.toInt()
    }
    return found
}

private fun probeMp4(b: ByteArray, len: Int, m: Builder) {
    m.kind = Kind.MP4
    for (moov in findBoxes(b, 0, len, "moov")) {
        for (mvhd in findBoxes(b, moov.start, moov.end, "mvhd")) {
            val version = b[mvhd.start].toInt() and 0xFF
            if (version == 0 && mvhd.end - mvhd.start >= 20) {
                val timescale = u32be(b, mvhd.start + 12)
                val duration = u32be(b, mvhd.start + 16)
                if (timescale > 0) m.durationSec = duration.toDouble() / timescale
            } else if (version == 1 && mvhd.end - mvhd.start >= 32) {
                val timescale = u32be(b, mvhd.start + 20)
                // 64-bit duration as two 32-bit halves.
                val duration = u32be(b, mvhd.start + 24) * 4294967296.0 + u32be(b, mvhd.start + 28)
                if (timescale > 0) m.durationSec = duration / timescale
            }
        }
        for (trak in findBoxes(b, moov.start, moov.end, "trak")) {
            val tkhds = findBoxes(b, trak.start, trak.end, "tkhd")
            if (tkhds.isNotEmpty() && b[tkhds[0].start].toInt() == 0
                && tkhds[0].end - tkhds[0].start >= 84) {
                // Width/height ride in the high 16 bits of fixed-point 16.16 values.
                m.width = u32be(b, tkhds[0].start + 76) / 65536.0
                m.height = u32be(b, tkhds[0].start + 80) / 65536.0
            }
            if (m.width != null) break  // first track is enough
        }
    }
}

private fun syncsafe(b: ByteArray, o: Int) =
    ((b[o].toInt() and 0x7F) shl 21) or ((b[o + 1].toInt() and 0x7F) shl 14) or
        ((b[o + 2].toInt() and 0x7F) shl 7) or (b[o + 3].toInt() and 0x7F)

/** ID3 text frame: encoding byte (0 latin1, 3 utf-8), then the text. */
private fun id3Text(b: ByteArray, start: Int, len: Int): String {
    if (len <= 1) return ""
    val enc = b[start].toInt() and 0xFF
    val text = b.copyOfRange(start + 1, start + len)
    val n = text.indexOf(0).let { if (it < 0) text.size else it }  // null terminator
    // Encodings 1/2 (UTF-16) live in the TS reference; latin-1 + UTF-8 cover
    // the practical files.
    return String(text, 0, n, if (enc == 3) StandardCharsets.UTF_8 else StandardCharsets.ISO_8859_1)
}

private fun probeId3(b: ByteArray, len: Int, m: Builder) {
    m.kind = Kind.MP3
    val tagSize = syncsafe(b, 6)
    val end = minOf(10 + tagSize, len)
    var pos = 10
    while (pos + 10 <= end) {
        val id = ascii(b, pos, 4)
        if (id[0] == '') break  // padding — padding bytes are NULs, blank in latin-1
        val frameSize = syncsafe(b, pos + 4)  // v2.4 syncsafe; v2.3 differs
        if (frameSize == 0 || pos + 10 + frameSize > end) break
        val text = id3Text(b, pos + 10, frameSize)
        if (id == "TIT2" && text.isNotEmpty()) m.title = text
        if (id == "TPE1" && text.isNotEmpty()) m.artist = text
        if (id == "TALB" && text.isNotEmpty()) m.album = text
        pos += 10 + frameSize
    }
}

fun probe(bytes: ByteArray): MediaMeta {
    if (bytes.size < 12) return MediaMeta()  // too short for ANY signature
    val m = Builder()
    val at4 = ascii(bytes, 4, 4)

    when {
        at4 == "ftyp" -> probeMp4(bytes, bytes.size, m)
        ascii(bytes, 0, 3) == "ID3" -> probeId3(bytes, bytes.size, m)
        ascii(bytes, 0, 4) == "RIFF" && at4 == "WAVE" -> {
            m.kind = Kind.WAV
            val byteRate = u32le(bytes, 28)  // PCM fmt chunk: byteRate at offset 28
            if (byteRate > 0 && bytes.size > 44)
                m.durationSec = (bytes.size - 44) / byteRate.toDouble()
        }
        ascii(bytes, 0, 4) == "GIF8" -> {
            m.kind = Kind.GIF
            m.width = u16le(bytes, 6).toDouble()   // logical screen descriptor
            m.height = u16le(bytes, 8).toDouble()
        }
        (bytes[0].toInt() and 0xFF) == 0x1A && (bytes[1].toInt() and 0xFF) == 0x45 &&
            (bytes[2].toInt() and 0xFF) == 0xDF && (bytes[3].toInt() and 0xFF) == 0xA3 -> {
            m.kind = Kind.WEBM
            // Duration lives in a float SecsInfo element — the TS lib scans
            // for it; this port flags the kind (documented gap).
        }
    }
    return m.build()
}

fun main() {
    // Smoke: a minimal MP4-sized buffer with an ftyp brand at offset 4.
    val mp4 = ByteArray(32).also {
        "ftypisom".toByteArray(StandardCharsets.ISO_8859_1).copyInto(it, 4)
    }
    println(probe(mp4).kind)       // MP4
    println(probe(ByteArray(3)).kind)  // UNKNOWN — too short
}

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 →