Skip to content

Instantly share code, notes, and snippets.

@mganeko
Last active Apr 23, 2022
Embed
What would you like to do?
Parse Binary of WebM file with Node.js
//
// This code parses binary format of WebM file.
// recognizes only some important TAGs
//
// Limitation:
// This programs reads all binary at once in memory (100MB).
// It is very bad imprementation, but it is still enough for some small WebM file.
//
// Usage:
// node parse_webm.js filename
//
// Refer:
// http://www.matroska.org/technical/specs/index.html
//
var path = require('path');
var fs = require('fs');
var util = require('util');
// check args
var args = process.argv.length;
if (args != 3) {
console.log('usage: node parse_webm.js filename');
return -1;
}
var filename = process.argv[2];
console.log('START parsing: ' + filename);
var fd = fs.openSync(filename, 'r');
var buf_size = 100*1024*1024; // 100MB
var buffer = new Buffer(buf_size);
var position = 0;
//var tagBytes[4];
var tagSize = 0;
var dataSizeSize = 0;
var dataSize = 0;
var tagDictionary = setupTagDictionary();
var readBytes = fs.readSync(fd, buffer, 0, buf_size, 0);
console.log('readBytes=' + readBytes + " " + addrHex(readBytes));
console.log(' ');
// --- parse webm ----
parseWebm(0, buffer, position, readBytes);
console.log('---- END ----- ');
return 0;
// ============
function parseWebm(level, buffer, position, maxPosition) {
while (position < maxPosition) {
var spc = spacer(level);
// -- ADDRESS --
console.log(spc + 'ADDR 0x' + addrHex(position) + ' -- Level:' + level + ' BEGIN' );
// -- TAG --
var result = scanWebmTag(buffer, position);
if (! result) {
console.log(spc + 'TAG scan end');
break;
}
var tagName = tagDictionary[result.str];
console.log(spc + 'Tag size=' + result.size + ' Tag=' + result.str + ' <' + tagName + '> TagVal=' + result.value);
position += result.size;
// --- DATA SIZE ---
result = scanDataSize(buffer, position);
if (! result) {
console.log(spc + 'DATA SIZE scan end');
break;
}
console.log(spc + 'DataSize size=' + result.size + ' DataSize str=' + result.str + ' DataSize Val=' + result.value);
position += result.size;
// ---- DATA ----
if (tagName === 'EBML') {
parseWebm(level+1, buffer, position, (position + result.value));
}
else if (tagName === 'Tracks') {
parseWebm(level+1, buffer, position, (position + result.value));
}
else if (tagName === 'TrackEntry') {
parseWebm(level+1, buffer, position, (position + result.value));
}
else if (tagName === 'CodecName') {
var codec = scanDataUTF8(buffer, position, result.value);
console.log(spc + 'codecName=' + codec);
}
else if (tagName === 'Video') {
parseWebm(level+1, buffer, position, (position + result.value));
}
else if (tagName === 'PixelWidth') {
var width = scanDataValueU(buffer, position, result.value);
console.log(spc + 'pixelWidth=' + width);
}
else if (tagName === 'PixelHeight') {
var height = scanDataValueU(buffer, position, result.value);
console.log(spc + 'pixelHeight=' + height);
}
else if (tagName === 'FrameRate') {
var rate = scanDataFloat(buffer, position, result.value);
console.log(spc + 'frameRate=' + rate);
}
else if (tagName === 'Segment') {
parseWebm(level+1, buffer, position, (position + result.value));
}
else if (tagName === 'Cluster') {
parseWebm(level+1, buffer, position, (position + result.value));
}
else if (tagName === 'Timecode') {
var timecode = scanDataValueU(buffer, position, result.value);
console.log(spc + 'timecode=' + timecode);
return position;
}
if (result.value >= 0) {
position += result.value;
}
else {
console.log(spc + 'DATA SIZE ffffffff.. cont.')
}
console.log(' ');
// -- check EOF ---
if (position == maxPosition) {
console.log(spc + '--level:' + level + ' reached END---');
break;
}
else if (position > maxPosition) {
console.log(spc + '--level:' + level + ' --OVER END---' + ' pos=' + position + ' max=' + maxPosition );
break;
}
}
return position;
}
function addrHex(pos) {
var str = '00000000' + pos.toString(16);
var len = str.length;
return str.substring(len - 8).toUpperCase();
}
function byteToHex(b) {
var str = '0' + b.toString(16);
var len = str.length;
return str.substring(len - 2).toUpperCase();
}
function spacer(level) {
var str = ' ';
str = str.substring(0, level);
return str;
}
function setupTagDictionary() {
// T - Element Type - The form of data the element contains. m: Master, u: unsigned int, i: signed integer, s: string, 8: UTF-8 string, b: binary, f: float, d: date
var tagDict = new Array();
tagDict['[1A][45][DF][A3]'] = 'EBML'; // EBML 0 [1A][45][DF][A3] m
tagDict['[42][86]'] = 'EBMLVersion'; //EBMLVersion 1 [42][86] u
tagDict['[42][F7]'] = 'EBMLReadVersion'; // EBMLReadVersion 1 [42][F7] u
tagDict['[42][F2]'] = 'EBMLMaxIDLength'; // EBMLMaxIDLength 1 [42][F2] u
tagDict['[42][F3]'] = 'EBMLMaxSizeLength'; // EBMLMaxSizeLength 1 [42][F3] u
tagDict['[42][82]'] = 'DocType'; // DocType 1 [42][82] s
tagDict['[42][87]'] = 'DocTypeVersion'; // DocTypeVersion 1 [42][87] u
tagDict['[42][85]'] = 'DocTypeReadVersion'; // DocTypeReadVersion 1 [42][85] u
tagDict['[EC]'] = 'Void'; // Void g [EC] b
tagDict['[BF]'] = 'CRC-32'; // CRC-32 g [BF] b
tagDict['[1C][53][BB][6B]'] = 'Cues'; // Cues 1 [1C][53][BB][6B] m
tagDict['[18][53][80][67]'] = 'Segment'; // Segment 0 [18][53][80][67] m
tagDict['[11][4D][9B][74]'] = 'SeekHead'; // SeekHead 1 [11][4D][9B][74] m
tagDict['[4D][BB]'] = 'Seek'; // Seek 2 [4D][BB] m
tagDict['[53][AB]'] = 'SeekID'; // SeekID 3 [53][AB] b
tagDict['[53][AC]'] = 'SeekPosition'; // SeekPosition 3 [53][AC] u
tagDict['[15][49][A9][66]'] = 'Info'; // Info 1 [15][49][A9][66] m
tagDict['[16][54][AE][6B]'] = 'Tracks'; // Tracks 1 [16][54][AE][6B] m
tagDict['[AE]'] = 'TrackEntry'; // TrackEntry 2 [AE] m
tagDict['[D7]'] = 'TrackNumber'; // TrackNumber 3 [D7] u
tagDict['[73][C5]'] = 'TrackUID'; // TrackUID 3 [73][C5] u
tagDict['[83]'] = 'TrackType'; // TrackType 3 [83] u
tagDict['[23][E3][83]'] = 'DefaultDuration'; // DefaultDuration 3 [23][E3][83] u
tagDict['[23][31][4F]'] = 'TrackTimecodeScale'; // TrackTimecodeScale 3 [23][31][4F] f
tagDict['[86]'] = 'CodecID'; // CodecID 3 [86] s
tagDict['[63][A2]'] = 'CodecPrivate'; // CodecPrivate 3 [63][A2] b
tagDict['[25][86][88]'] = 'CodecName'; // CodecName 3 [25][86][88] 8
tagDict['[E0]'] = 'Video'; // Video 3 [E0] m
tagDict['[B0]'] = 'PixelWidth'; // PixelWidth 4 [B0] u
tagDict['[BA]'] = 'PixelHeight'; // PixelHeight 4 [BA] u
tagDict['[23][83][E3]'] = 'FrameRate'; // FrameRate 4 [23][83][E3] f
tagDict['[E1]'] = 'Audio'; // Audio 3 [E1] m
tagDict['[B5]'] = 'SamplingFrequency'; // SamplingFrequency 4 [B5] f
tagDict['[9F]'] = 'Channels'; // Channels 4 [9F] u
tagDict['[1F][43][B6][75]'] = 'Cluster'; // Cluster 1 [1F][43][B6][75] m
tagDict['[E7]'] = 'Timecode'; // Timecode 2 [E7] u
tagDict['[A3]'] = 'SimpleBlock'; // SimpleBlock 2 [A3] b
return tagDict;
}
function scanWebmTag(buff, pos) {
var tagSize = 0;
var followByte;
var firstByte = buff.readUInt8(pos);
var firstMask = 0xff;
if (firstByte & 0x80) {
tagSize = 1;
}
else if (firstByte & 0x40) {
tagSize = 2;
}
else if (firstByte & 0x20) {
tagSize = 3;
}
else if (firstByte & 0x10) {
tagSize = 4;
}
else {
console.log('ERROR: bad TAG byte');
return null;
}
var decodeRes = decodeBytes(buff, pos, tagSize, firstByte, firstMask);
return decodeRes;
}
function scanDataSize(buff, pos) {
var dataSizeSize = 0;
var followByte;
var firstByte = buff.readUInt8(pos);
var firstMask;
if (firstByte & 0x80) {
dataSizeSize = 1;
firstMask = 0x7f;
}
else if (firstByte & 0x40) {
dataSizeSize = 2;
firstMask = 0x3f;
}
else if (firstByte & 0x20) {
dataSizeSize = 3;
firstMask = 0x1f;
}
else if (firstByte & 0x10) {
dataSizeSize = 4;
firstMask = 0x0f;
}
else if (firstByte & 0x08) {
dataSizeSize = 5;
firstMask = 0x07;
}
else if (firstByte & 0x04) {
dataSizeSize = 6;
firstMask = 0x03;
}
else if (firstByte & 0x02) {
dataSizeSize = 7;
firstMask = 0x01;
}
else if (firstByte & 0x01) {
dataSizeSize = 8;
firstMask = 0x00;
}
else {
console.log('ERROR: bad DATA byte');
return null;
}
var decodeRes = decodeBytes(buff, pos, dataSizeSize, firstByte, firstMask);
return decodeRes;
}
function scanDataValueU(buff, pos, size) {
var uVal = 0;
var byteVal;
for (var i = 0; i < size; i++) {
byteVal = buff.readUInt8(pos + i);
//console.log('scanDataValueU pos=' + pos + ' i=' + i + ' byte=' + byteToHex(byteVal));
uVal = (uVal << 8) + byteVal;
}
return uVal;
}
function scanDataUTF8(buff, pos, size) {
var sVal = buff.toString('utf8', pos, pos+size);
return sVal;
}
function scanDataFloat(buff, pos, size) {
if (size === 4) {
var f = buff.readFloatBE(pos);
return f;
}
else if (size === 8) {
var df = buff.readDoubleBE(pos);
return df;
}
else {
console.error('ERROR. Bad Float size=' + size);
return null;
}
}
function decodeBytes(buff, pos, size, firstByte, firstMask) {
var value = firstByte & firstMask;
var str = ('[' + byteToHex(firstByte) + ']');
var followByte;
for (var i = 1; i < size; i++) {
followByte = buff.readUInt8(pos + i);
str += '[';
str += byteToHex(followByte);
str += ']';
value = (value << 8) + followByte;
}
var res = {};
res.str = str;
res.size = size;
res.value = value;
return res;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment