├── test.js ├── package.json ├── README.md └── mkvdemux.js /test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require("fs"); 3 | const mkvdemuxjs = require("./mkvdemux.js"); 4 | const mkv = new mkvdemuxjs.MkvDemux(); 5 | const test = fs.readFileSync("test.mkv").buffer; 6 | 7 | mkv.push(test); 8 | 9 | (function() { 10 | var el; 11 | while (el = mkv.demux()) { 12 | if (el.track) 13 | console.log(el); 14 | if (el.frames) 15 | console.log(el.frames[0]); 16 | } 17 | })(); 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mkvdemuxjs", 3 | "version": "1.0.1", 4 | "description": "MKV demuxer in pure JavaScript", 5 | "main": "mkvdemux.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Yahweasel/mkvdemuxjs.git" 12 | }, 13 | "keywords": [ 14 | "matroska", 15 | "mkv", 16 | "demuxer", 17 | "media", 18 | "audio", 19 | "video", 20 | "webm" 21 | ], 22 | "author": "Yahweasel", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/Yahweasel/mkvdemuxjs/issues" 26 | }, 27 | "homepage": "https://github.com/Yahweasel/mkvdemuxjs#readme" 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a demuxer for Matroska (and thus WebM) files, designed to be exactly 2 | sufficient for demuxing the WebM files generated by MediaRecorder, and nothing 3 | else. As a consequence, it's capable of live-demuxing data fed to it and giving 4 | that information back in a semi-useful way, and nothing more. 5 | 6 | NOTE: This library is no longer maintained. The author instead uses [a port of 7 | FFmpeg's libav](https://github.com/Yahweasel/libav.js/), which is far more 8 | general and powerful, and has a negligible performance impact for the task of 9 | demuxing. 10 | 11 | To use it, create an MkvDemux object, then push ArrayBuffer frames, and call 12 | demux to demux them. For instance: 13 | 14 | ```javascript 15 | let mkvDemuxer = new mkvdemuxjs.MkvDemux(); 16 | let part = null; 17 | mkvDemuxer.push(chunk); 18 | while ((part = mkvDemuxer.demux()) !== null) { 19 | // Do something with part 20 | } 21 | ``` 22 | 23 | Since it's intended to stream, you can push partial data and push more data 24 | whenever you have it. The `demux` function will return `null` when no new data 25 | is available. 26 | 27 | The parts that `demux` returns are in various forms. If it encountered a track 28 | description, it returns an object like so: 29 | 30 | ```javascript 31 | { 32 | "track": { 33 | "number": 1, 34 | "type": "audio", 35 | "sampleRate": 48000, 36 | ... etc ... 37 | } 38 | } 39 | ``` 40 | 41 | If it encountered a block of frames, it returns an object like so: 42 | 43 | ```javascript 44 | { 45 | "frames": [ 46 | { 47 | timestamp: (in seconds, floating point), 48 | track: (track number), 49 | data: (ArrayBuffer) 50 | } 51 | ] 52 | } 53 | ``` 54 | 55 | For everything else, it returns an internal representation of the EBML tag, 56 | which is likely not useful. 57 | -------------------------------------------------------------------------------- /mkvdemux.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Yahweasel 3 | * 4 | * Permission to use, copy, modify, and/or distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 11 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 13 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 14 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | var mkvdemuxjs = (function(mkvdemuxjs) { 18 | // A few IDs we repeat, as "globals" 19 | var ID_CLUSTER = 0x1f43b675; 20 | var ID_TRACKENTRY = 0xae; 21 | 22 | // The main MKV demuxer type 23 | function MkvDemux() { 24 | // Queue of data to be demuxed 25 | this.queue = []; 26 | this.peekHi = 0; 27 | this.peekLo = 0; 28 | 29 | // Our overall position in the document 30 | this.pos = 0; 31 | 32 | // Our entire context 33 | this.context = []; 34 | 35 | // Our current context 36 | this.block = null; 37 | 38 | // Extra info used during demuxing 39 | this.ex = {}; 40 | } 41 | 42 | MkvDemux.prototype = { 43 | // Push ArrayBuffer data in the queue 44 | push: function(data) { 45 | this.queue.push(data); 46 | }, 47 | 48 | // Peek this many bytes from the queue 49 | peek: function(count) { 50 | if (this.queue.length <= this.peekHi) 51 | return null; 52 | 53 | // Special case for peeking at the beginning 54 | var q = this.queue[this.peekHi]; 55 | if (this.peekLo === 0 && q.byteLength >= count) { 56 | this.peekLo += count; 57 | if (this.peekLo >= q.byteLength) { 58 | this.peekHi++; 59 | this.peekLo = 0; 60 | } 61 | return q; 62 | } 63 | 64 | // Special case for peeking with enough data left 65 | if (q.byteLength >= this.peekLo + count) { 66 | var ret = new Uint8Array(q).slice(this.peekLo, this.peekLo + count); 67 | this.peekLo += count; 68 | if (this.peekLo >= q.byteLength) { 69 | this.peekHi++; 70 | this.peekLo = 0; 71 | } 72 | return ret.buffer; 73 | } 74 | 75 | // Otherwise, construct our return 76 | var ret = new Uint8Array(count); 77 | ret.set(new Uint8Array(q).slice(this.peekLo)); 78 | var pos = q.byteLength - this.peekLo; 79 | var left = count - pos; 80 | var i = this.peekHi + 1; 81 | while (left) { 82 | if (i >= this.queue.length) return null; 83 | 84 | // Pull out another buffer 85 | var next = new Uint8Array(this.queue[i++]); 86 | if (next.length >= left) { 87 | // This has enough to fill out the remainder 88 | if (next.length === left) { 89 | // Exactly enough 90 | ret.set(next, pos); 91 | this.peekHi = i + 1; 92 | this.peekLo = 0; 93 | left = 0; 94 | 95 | } else { 96 | // More than enough 97 | ret.set(next.slice(0, left), pos); 98 | this.peekHi = i; 99 | this.peekLo = left; 100 | left = 0; 101 | 102 | } 103 | 104 | } else { 105 | // Not enough, need the whole thing 106 | ret.set(next, pos); 107 | left -= next.length; 108 | pos += next.length; 109 | 110 | } 111 | } 112 | return ret.buffer; 113 | }, 114 | 115 | // Commit whatever we've peeked 116 | commit: function() { 117 | // Remove entire elements 118 | while (this.peekHi) { 119 | this.pos += this.queue[0].byteLength; 120 | this.queue.shift(); 121 | this.peekHi--; 122 | } 123 | 124 | // And truncate the last one 125 | if (this.peekLo) { 126 | this.pos += this.peekLo; 127 | var nq = new Uint8Array(this.queue[0]).slice(this.peekLo); 128 | if (nq.length === 0) 129 | this.queue.shift(); 130 | else 131 | this.queue[0] = nq.buffer; 132 | this.peekLo = 0; 133 | } 134 | 135 | // If we've moved beyond any elements, pop them from our context 136 | while (this.context.length && this.context[this.context.length-1].end <= this.pos) 137 | this.context.pop(); 138 | if (this.context.length) 139 | this.block = this.context[this.context.length-1]; 140 | else 141 | this.block = null; 142 | }, 143 | 144 | // Save our peek position 145 | savePeek: function() { 146 | return {hi: this.peekHi, lo: this.peekLo}; 147 | }, 148 | 149 | // Restore our peek position 150 | restorePeek: function(from) { 151 | this.peekHi = from.hi; 152 | this.peekLo = from.lo; 153 | }, 154 | 155 | // Peek a variable-sized int (VINT) 156 | peekVint: function(keepMarker) { 157 | /* variable-size ints have a header that describes the width, then 158 | * the data across multiple bytes */ 159 | var pre = this.savePeek(); 160 | var header = this.peek(1); 161 | if (header === null) 162 | return null; 163 | header = new Uint8Array(header)[0]; 164 | 165 | // Determine how many bytes are represented by this header 166 | var bytes = 1; 167 | while (!(header & 0x80)) { 168 | bytes++; 169 | header = (header << 1) & 0xFF; 170 | } 171 | 172 | // Get rid of the 1 bit 173 | header &= 0x7F; 174 | header >>>= bytes-1; 175 | 176 | // Now read in the whole thing 177 | this.restorePeek(pre); 178 | var whole = this.peek(bytes); 179 | if (whole === null) 180 | return null; 181 | 182 | // Then convert it 183 | whole = new Uint8Array(whole).slice(0, bytes); 184 | if (!keepMarker) 185 | whole[0] = header; 186 | var ret = 0; 187 | for (var i = 0; i < whole.length; i++) { 188 | ret *= 0x100; 189 | ret += whole[i]; 190 | } 191 | 192 | return ret; 193 | }, 194 | 195 | // Read an EBML header 196 | readEBMLHeader: function() { 197 | var pre = this.savePeek(); 198 | var prePos = this.pos; 199 | 200 | // ID 201 | var id = this.peekVint(true); 202 | if (id === null) 203 | return null; 204 | 205 | var length; 206 | 207 | // Check for the special case of unknown length, which we just ignore in our context 208 | var pre2 = this.savePeek(); 209 | var x = this.peek(1); 210 | if (x === null) { 211 | this.restorePeek(pre); 212 | return null; 213 | } 214 | x = new Uint8Array(x)[0]; 215 | if (x === 0xBF) { 216 | // Unknown length 217 | length = -1; 218 | } else { 219 | // Normal VINT 220 | this.restorePeek(pre2); 221 | length = this.peekVint(); 222 | if (length === null) { 223 | this.restorePeek(pre); 224 | return null; 225 | } 226 | } 227 | 228 | // Update our context 229 | this.commit(); 230 | var ret = new EBML(id, length, prePos, this.pos); 231 | if (length >= 0) { 232 | this.context.push(ret); 233 | this.block = ret; 234 | } 235 | 236 | return ret; 237 | }, 238 | 239 | // Read the entire content of the current EBML element 240 | readEBMLBody: function() { 241 | if (this.block.length === 0) { 242 | this.context.pop(); 243 | if (this.context.length) 244 | this.block = this.context[this.context.length-1]; 245 | else 246 | this.block = null; 247 | return null; 248 | } 249 | 250 | // Read it in 251 | var hdr = this.block; 252 | var len = hdr.end - this.pos; 253 | var ret = this.peek(len); 254 | if (ret === null) 255 | return null; 256 | 257 | // Read it, so commit 258 | this.commit(); 259 | return ret; 260 | }, 261 | 262 | // Read the content of the current EBML element as an unsigned integer 263 | readUInt: function() { 264 | var len = this.block.length; 265 | 266 | // Read it in 267 | var val = this.readEBMLBody(); 268 | if (len === 0) 269 | return 0; 270 | if (val === null) 271 | return null; 272 | 273 | // Convert it 274 | val = new DataView(val); 275 | switch (len) { 276 | case 1: 277 | return val.getUint8(0); 278 | case 2: 279 | return val.getUint16(0); 280 | case 4: 281 | return val.getUint32(0); 282 | 283 | default: 284 | var ret = 0; 285 | val = new Uint8Array(val.buffer); 286 | for (var i = 0; i < len; i++) { 287 | ret *= 0x100; 288 | ret += val[i]; 289 | } 290 | return ret; 291 | } 292 | }, 293 | 294 | // Read the content of the current EBML element as a float 295 | readFloat: function() { 296 | var len = this.block.length; 297 | 298 | // Read it in 299 | var val = this.readEBMLBody(); 300 | if (len === 0) 301 | return 0; 302 | if (val === null) 303 | return null; 304 | 305 | // Convert it 306 | val = new DataView(val); 307 | if (len === 4) 308 | return val.getFloat32(0); 309 | else if (len === 8) 310 | return val.getFloat64(0); 311 | else 312 | return 0; 313 | }, 314 | 315 | /* The main demuxer. Can return one of several structures: 316 | * If there's not enough data to demux, returns null. 317 | * If it received a track entry, returns 318 | * {track: {...}} 319 | * If it received packets, returns 320 | * {frames: array({data: [data], track: [track number], timestamp: [timestamp]})} 321 | * If it received anything else, returns the EBML context 322 | */ 323 | demux: function() { 324 | var el; 325 | 326 | while (true) { 327 | // Figure out our current context 328 | if (!this.context.length) 329 | this.readEBMLHeader(); 330 | if (!this.context.length) 331 | return null; 332 | el = this.block; 333 | 334 | // We should know what to read based on that context 335 | switch (el.id) { 336 | case 0x1a45dfa3: // EBML header 337 | case 0x114d9b74: // SeekHead 338 | case 0x1549a966: // Info 339 | case 0x1254c367: // Tags 340 | case 0x75a2: // DiscardPadding 341 | case 0xec: // ??? 342 | case 0xbf: // ??? 343 | // Elements we don't care about. Skip them. 344 | var ct = this.readEBMLBody(); 345 | if (ct === null) 346 | return null; 347 | el.ex.content = ct; 348 | return el; 349 | 350 | case 0x18538067: // Segment 351 | case 0x1654ae6b: // Tracks 352 | case ID_CLUSTER: // Cluster 353 | case 0xa0: // BlockGroup 354 | // Surrounding elements we need to dig into 355 | if (this.readEBMLHeader() === null) 356 | return null; 357 | this.ex = {}; 358 | break; 359 | 360 | case ID_TRACKENTRY: // TrackEntry 361 | // A track description. Read it fully. 362 | return this.readTrackEntry(); 363 | 364 | case 0xe7: // Timestamp (in cluster) 365 | var val = this.readUInt(); 366 | if (val === null) 367 | return null; 368 | this.ex.clusterTimestamp = val; 369 | break; 370 | 371 | case 0xa1: // Block 372 | case 0xa3: // SimpleBlock 373 | return this.decodeBlock(); 374 | 375 | default: 376 | // We don't know what this is! 377 | var ct = this.readEBMLBody(); 378 | if (ct === null) 379 | return null; 380 | console.log("Unrecognized element " + el.id.toString(16) + " at " + el.start.toString(16) + " to " + el.end.toString(16)); 381 | console.log("Context:"); 382 | for (var i = 0; i < this.context.length; i++) 383 | console.log(" " + this.context[i].id.toString(16)); 384 | el.ex.content = ct; 385 | return el; 386 | } 387 | } 388 | }, 389 | 390 | // Read and decode a track entry 391 | readTrackEntry: function() { 392 | var te = this.block; 393 | var len = te.end - this.pos; 394 | 395 | // Make sure it's all here 396 | var pre = this.savePeek(); 397 | if (this.peek(len) === null) 398 | return null; 399 | this.restorePeek(pre); 400 | 401 | var ret = {track: {}}; 402 | 403 | // Now read in each of the parts 404 | while (this.block === te) { 405 | var hdr = this.readEBMLHeader(); 406 | if (hdr === null) 407 | return null; 408 | 409 | switch (hdr.id) { 410 | case 0x9c: // FlagLacing 411 | case 0x22b59c: // Language 412 | case 0x56bb: // SeekPreRoll 413 | case 0x63a2: // CodecPrivate 414 | // Irrelevant for us, just skip it 415 | this.readEBMLBody(); 416 | break; 417 | 418 | case 0xe1: // Audio 419 | // We need the actual body of this 420 | this.context.pop(); 421 | this.block = te; 422 | break; 423 | 424 | case 0xd7: // TrackNumber 425 | ret.track.number = this.readUInt(); 426 | break; 427 | 428 | case 0x73c5: // TrackUID 429 | ret.track.uid = this.readUInt(); 430 | break; 431 | 432 | case 0x86: // CodecID 433 | ret.track.codec = this.readUInt(); 434 | break; 435 | 436 | case 0x56aa: // CodecDelay 437 | ret.track.codecDelay = this.readUInt() / 1000000000; 438 | break; 439 | 440 | case 0x83: // CodecType 441 | var ct = ret.track.codecType = this.readUInt(); 442 | var t = "unknown"; 443 | switch (ct) { 444 | case 1: 445 | t = "video"; 446 | break; 447 | 448 | case 2: 449 | t = "audio"; 450 | break; 451 | } 452 | ret.track.type = t; 453 | break; 454 | 455 | case 0x9f: // Audio:Channels 456 | ret.track.channels = this.readUInt(); 457 | break; 458 | 459 | case 0xb5: // Audio:SamplingFrequency 460 | ret.track.sampleRate = this.readFloat(); 461 | break; 462 | 463 | case 0x6264: // Audio:BitDepth 464 | ret.track.bitDepth = this.readUInt(); 465 | break; 466 | 467 | default: 468 | console.log("Unrecognized track entry component " + hdr.id.toString(16)); 469 | this.readEBMLBody(); 470 | } 471 | } 472 | 473 | return ret; 474 | }, 475 | 476 | // Decode a single Block or SimpleBlock 477 | decodeBlock: function() { 478 | var len = this.block.length; 479 | var cont = this.readEBMLBody(); 480 | if (cont === null) 481 | return null; 482 | cont = new DataView(cont); 483 | 484 | // Surrounding data 485 | var ret = {}; 486 | var clusterTimestamp = this.ex.clusterTimestamp; 487 | if (!clusterTimestamp) 488 | clusterTimestamp = 0; 489 | 490 | // Byte 0: Track number 491 | var track = cont.getUint8(0); 492 | if (!(track & 0x80)) 493 | console.log("ERROR: Track #s greater than 127 are not supported!"); 494 | track &= 0x7F; 495 | 496 | // Bytes 1 and 2: Timecode offset 497 | var timeOff = cont.getInt16(1); 498 | var timestamp = (clusterTimestamp + timeOff) / 1000; 499 | 500 | // Byte 3: Flags 501 | var flags = cont.getUint8(3); 502 | if ((flags & 0x6) !== 0) 503 | console.log("ERROR: Lacing is not supported!"); 504 | 505 | // Rest: The actual data 506 | var frame = { 507 | data: new Uint8Array(cont.buffer).slice(4, len).buffer, 508 | track: track, 509 | timestamp: timestamp 510 | }; 511 | ret.frames = [frame]; 512 | 513 | return ret; 514 | } 515 | }; 516 | 517 | // An EBML "tag" 518 | function EBML(id, length, start, bodyStart) { 519 | this.id = id; 520 | this.length = length; 521 | this.start = start; 522 | this.bodyStart = bodyStart; 523 | if (length >= 0) 524 | this.end = bodyStart + length; 525 | else 526 | this.end = Infinity; 527 | this.ex = {}; // Extra context stored by the main demuxer 528 | } 529 | 530 | mkvdemuxjs.MkvDemux = MkvDemux; 531 | 532 | return mkvdemuxjs; 533 | 534 | })(mkvdemuxjs || {}); 535 | 536 | if (typeof module !== "undefined") 537 | module.exports = mkvdemuxjs; 538 | --------------------------------------------------------------------------------