Last active
May 21, 2024 03:06
-
-
Save mganeko/9ceee931ac5dde298e81 to your computer and use it in GitHub Desktop.
Parse Binary of WebM file with Node.js
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
// | |
// 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