// // 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; }