Skip to content

Instantly share code, notes, and snippets.

@19h
Last active June 25, 2023 02:06
Show Gist options
  • Save 19h/ab812f6121510e850ff1a9d2da861f7e to your computer and use it in GitHub Desktop.
Save 19h/ab812f6121510e850ff1a9d2da861f7e to your computer and use it in GitHub Desktop.
MP4 tkhd parser, works no matter how fucked your mp4 buffer is as long as it contains a tkhd box -- will give you dimensions (width, height), duration, creation time, modification time, track id, layer, alternate group, volume, the entire matrix, flags and version of the mp4 file.
const readU8 = (data, offset) =>
data[offset];
const readU16 = (data, offset) =>
(
data[offset] << 8
| data[offset + 1]
);
const readU24 = (data, offset) =>
(
data[offset] << 16
| data[offset + 1] << 8
| data[offset + 2]
);
const readU32 = (data, offset) =>
(
BigInt(data[offset]) << 24n
| BigInt(data[offset + 1]) << 16n
| BigInt(data[offset + 2]) << 8n
| BigInt(data[offset + 3])
);
const readU64 = (data, offset) =>
BigInt(readU32(data, offset)) << 32n
| BigInt(readU32(data, offset + 4));
const readTkhd = data => {
let offset = 0;
const version = readU8(data, offset);
offset += 1;
const flags = readU24(data, offset);
offset += 3;
const trackSizeIsAspectRatio = (flags & 0b001000) !== 0;
const trackInPreview = (flags & 0b000100) !== 0;
const trackInMovie = (flags & 0b000010) !== 0;
const trackEnabled = (flags & 0b000001) !== 0;
let creationTime;
let modificationTime;
let trackId;
let duration;
if (version === 1) {
[
creationTime,
modificationTime,
trackId,
duration,
] = [
readU64(data, offset),
readU64(data, offset + 8),
readU32(data, offset + 16),
// u32, reserved,
readU64(data, offset + 24)
];
offset += 28;
}
if (version === 0) {
[
creationTime,
modificationTime,
trackId,
duration,
] = [
readU32(data, offset),
readU32(data, offset + 4),
readU32(data, offset + 8),
// u32, reserved,
readU32(data, offset + 16),
];
offset += 20;
}
if (version !== 0 && version !== 1) {
return -1;
}
offset += 8; // reserved
const layer = readU16(data, offset);
offset += 2;
const alternateGroup = readU16(data, offset);;
offset += 2;
const volume = readU16(data, offset);
offset += 2;
offset += 2; // reserved
let a = readU32(data, offset);
let b = readU32(data, offset + 4);
let u = readU32(data, offset + 8);
let c = readU32(data, offset + 12);
let d = readU32(data, offset + 16);
let v = readU32(data, offset + 20);
let x = readU32(data, offset + 24);
let y = readU32(data, offset + 28);
let w = readU32(data, offset + 32);
offset += 36;
const matrix = {
a, b, u, c, d,
v, x, y, w,
};
const width = readU16(data, offset);
offset += 2;
offset += 2; // widthQ
const height = readU16(data, offset);
offset += 2;
return {
version,
flags: {
trackSizeIsAspectRatio,
trackInPreview,
trackInMovie,
trackEnabled,
},
creationTime,
modificationTime,
trackId,
duration,
layer,
alternateGroup,
volume,
matrix,
width,
height
};
}
const read_mp4_meta = (buf, offset = 0) => {
let i = offset;
while (i++ < buf.length) {
if (
i > 3 // 4 bytes for size
&& buf[i] === 0x74 // t
&& buf[i + 1] === 0x6B // k
&& buf[i + 2] === 0x68 // h
&& buf[i + 3] === 0x64 // d
) {
const tkhdSize = Number(readU32(buf, i - 4));
if (i + tkhdSize > buf.length) {
return null;
}
return readTkhd(
buf.subarray(
i + 4,
i + 4 + tkhdSize,
),
0,
);
}
}
return null;
}
module.exports.read_mp4_meta = read_mp4_meta;
const mp4_tkhd_hint = (buf, offset = 0) => {
let i = offset;
while (i++ < buf.length) {
if (
i > 3 // 4 bytes for size
&& buf[i] === 0x74 // t
&& buf[i + 1] === 0x6B // k
&& buf[i + 2] === 0x68 // h
&& buf[i + 3] === 0x64 // d
) {
const tkhdSize = Number(readU32(buf, i - 4));
if (i + tkhdSize > buf.length) {
return offset;
}
return i - 4;
}
}
return buf.length;
}
module.exports.mp4_tkhd_hint = mp4_tkhd_hint;
const { read_mp4_meta } = require('./parseMp4.js');
const buf =
Buffer.from([
0x00, 0x00, 0x00, 0x5C, 0x74, 0x6B, 0x68, 0x64,
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00,
0xDA, 0xF7, 0x89, 0x58, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB7, 0x49,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
0x02, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x42, 0x35
]);
console.log(
read_mp4_meta(buf),
);
// returns
//
// {
// version: 0,
// flags: {
// trackSizeIsAspectRatio: false,
// trackInPreview: false,
// trackInMovie: true,
// trackEnabled: true
// },
// creationTime: 0n,
// modificationTime: 3673655640n,
// trackId: 1n,
// duration: 46921n,
// layer: 0,
// alternateGroup: 0,
// volume: 0,
// matrix: {
// a: 65536n,
// b: 0n,
// u: 0n,
// c: 0n,
// d: 65536n,
// v: 0n,
// x: 0n,
// y: 0n,
// w: 1073741824n
// },
// width: 1280,
// height: 720
// }
// More complex stream example
//
// Streams a file from an external URL and
// queues chunks until we get the mp4 meta data.
const { Readable } = require('stream');
(async () => {
const x = await fetch('https://example.com/file.mp4');
const stream =
Readable.fromWeb(
x.body,
);
let analysisChunk = Buffer.alloc(0);
let tkhd_hint_offset = 0;
for await (const chunk of stream) {
tkhd_hint_offset =
mp4_tkhd_hint(
analysisChunk,
tkhd_hint_offset,
);
// hint was placed at the end of the chunk
// = we need to process the next chunk
if (tkhd_hint_offset === analysisChunk.length) {
analysisChunk = Buffer.concat([
analysisChunk,
chunk,
]);
tkhd_hint_offset =
mp4_tkhd_hint(
analysisChunk,
tkhd_hint_offset,
);
// have we found the tkhd box in the mp4 buffer?
if (tkhd_hint_offset < analysisChunk.length) {
console.log(
read_mp4_meta(
analysisChunk,
tkhd_hint_offset,
),
);
break;
}
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment