Last active
June 25, 2023 02:06
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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