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 →