#!/usr/bin/env node
/**
 * Report what an exported mp4 actually contains, per track: timescale, sample
 * count, and the sample-duration table (stts).
 *
 * The point is to separate "the exporter dropped frames" from "the player is
 * stuttering on a file that is fine". A 60 fps, N-second video must show
 * timescale 60 and one stts row of [N*60, 1]. Several rows — or any row with a
 * duration of 0 — means the muxed timeline is wrong, and the frames that are
 * there will not be shown.
 *
 *   node tools/probe-mp4.js path/to/export.mp4
 */
import fs from 'fs';

const path = process.argv[2];
if (!path) {
    console.error('usage: node tools/probe-mp4.js <file.mp4>');
    process.exit(1);
}
const buf = fs.readFileSync(path);

const CONTAINERS = ['moov', 'trak', 'mdia', 'minf', 'stbl'];
const tracks = [];
let cur = null;

function walk(start, end) {
    let off = start;
    while (off + 8 <= end) {
        let size = buf.readUInt32BE(off);
        const type = buf.toString('latin1', off + 4, off + 8);
        let hdr = 8;
        if (size === 1) { size = Number(buf.readBigUInt64BE(off + 8)); hdr = 16; }
        if (size === 0) size = end - off;
        if (CONTAINERS.includes(type)) {
            if (type === 'trak') { cur = {}; tracks.push(cur); }
            walk(off + hdr, off + size);
        } else if (cur) {
            box(type, off + hdr);
        }
        off += size;
    }
}

function box(type, s) {
    if (type === 'mdhd') {
        cur.timescale = buf.readUInt32BE(s + 12);
        cur.duration = buf.readUInt32BE(s + 16);
    } else if (type === 'hdlr') {
        cur.kind = buf.toString('latin1', s + 8, s + 12);
    } else if (type === 'stsd') {
        cur.format = buf.toString('latin1', s + 12, s + 16);
    } else if (type === 'stsz') {
        cur.sampleCount = buf.readUInt32BE(s + 8);
    } else if (type === 'stts') {
        const n = buf.readUInt32BE(s + 4);
        const rows = [];
        let samples = 0;
        let total = 0;
        for (let i = 0; i < n; i++) {
            const count = buf.readUInt32BE(s + 8 + i * 8);
            const delta = buf.readUInt32BE(s + 12 + i * 8);
            rows.push({ count, delta });
            samples += count;
            total += count * delta;
        }
        cur.stts = { rows, samples, total };
    }
}

walk(0, buf.length);

for (const t of tracks) {
    const stts = t.stts || { rows: [], samples: 0, total: 0 };
    const seconds = t.timescale ? stts.total / t.timescale : 0;
    console.log(`\n[${t.kind}] ${t.format} · timescale ${t.timescale}`);
    console.log(`  samples: ${t.sampleCount} · media duration: ${seconds.toFixed(3)}s`);
    if (t.kind === 'vide' && seconds > 0) {
        console.log(`  effective frame rate: ${(stts.samples / seconds).toFixed(3)} fps`);
    }
    console.log(`  stts rows (${stts.rows.length}):`);
    for (const r of stts.rows.slice(0, 20)) {
        console.log(`    ${r.count} × ${r.delta}${r.delta === 0 ? '   <-- zero duration: these frames never display' : ''}`);
    }
    if (stts.rows.length > 20) console.log(`    … ${stts.rows.length - 20} more rows`);
    const zeros = stts.rows.filter((r) => r.delta === 0).reduce((a, r) => a + r.count, 0);
    if (zeros) console.log(`  ${zeros} samples have zero duration`);
}
