#!/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 '); 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`); }