UNPKG

@jxstjh/jhvideo

Version:

HTML5 jhvideo base on MPEG2-TS Stream Player

1,096 lines (1,087 loc) 299 kB
(function (f) { if (typeof exports === "object" && typeof module !== "undefined") { module.exports = f() } else if (typeof define === "function" && define.amd) { define([], f) } else { var g; if (typeof window !== "undefined") { g = window } else if (typeof global !== "undefined") { g = global } else if (typeof self !== "undefined") { g = self } else { g = this } g.EBML = f() } })(function () { var define, module, exports; return (function () { function r(e, n, t) { function o(i, f) { if (!n[i]) { if (!e[i]) { var c = "function" == typeof require && require; if (!f && c) return c(i, !0); if (u) return u(i, !0); var a = new Error("Cannot find module '" + i + "'"); throw a.code = "MODULE_NOT_FOUND", a } var p = n[i] = { exports: {} }; e[i][0].call(p.exports, function (r) { var n = e[i][1][r]; return o(n || r) }, p, p.exports, r, e, n, t) } return n[i].exports } for (var u = "function" == typeof require && require, i = 0; i < t.length; i++)o(t[i]); return o } return r })()({ 1: [function (require, module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var tools_1 = require("./tools"); var int64_buffer_1 = require("int64-buffer"); var tools = require("./tools"); var schema = require("matroska/lib/schema"); var byEbmlID = schema.byEbmlID; // https://www.matroska.org/technical/specs/index.html var State; (function (State) { State[State["STATE_TAG"] = 1] = "STATE_TAG"; State[State["STATE_SIZE"] = 2] = "STATE_SIZE"; State[State["STATE_CONTENT"] = 3] = "STATE_CONTENT"; })(State || (State = {})); var EBMLDecoder = /** @class */ (function () { function EBMLDecoder() { this._buffer = new tools_1.Buffer(0); this._tag_stack = []; this._state = State.STATE_TAG; this._cursor = 0; this._total = 0; this._schema = byEbmlID; this._result = []; } EBMLDecoder.prototype.decode = function (chunk) { this.readChunk(chunk); var diff = this._result; this._result = []; return diff; }; EBMLDecoder.prototype.readChunk = function (chunk) { // 読みかけの(読めなかった) this._buffer と 新しい chunk を合わせて読み直す this._buffer = tools.concat([this._buffer, new tools_1.Buffer(chunk)]); while (this._cursor < this._buffer.length) { // console.log(this._cursor, this._total, this._tag_stack); if (this._state === State.STATE_TAG && !this.readTag()) { break; } if (this._state === State.STATE_SIZE && !this.readSize()) { break; } if (this._state === State.STATE_CONTENT && !this.readContent()) { break; } } }; EBMLDecoder.prototype.getSchemaInfo = function (tagNum) { return this._schema[tagNum] || { name: "unknown", level: -1, type: "unknown", description: "unknown" }; }; /** * vint された parsing tag * @return - return false when waiting for more data */ EBMLDecoder.prototype.readTag = function () { // tag.length が buffer の外にある if (this._cursor >= this._buffer.length) { return false; } // read ebml id vint without first byte var tag = tools_1.readVint(this._buffer, this._cursor); // tag が読めなかった if (tag == null) { return false; } // >>>>>>>>> // tag 識別子 //const tagStr = this._buffer.toString("hex", this._cursor, this._cursor + tag.length); //const tagNum = parseInt(tagStr, 16); // 上と等価 var buf = this._buffer.slice(this._cursor, this._cursor + tag.length); var tagNum = buf.reduce(function (o, v, i, arr) { return o + v * Math.pow(16, 2 * (arr.length - 1 - i)); }, 0); var schema = this.getSchemaInfo(tagNum); var tagObj = { EBML_ID: tagNum.toString(16), schema: schema, type: schema.type, name: schema.name, level: schema.level, tagStart: this._total, tagEnd: this._total + tag.length, sizeStart: this._total + tag.length, sizeEnd: null, dataStart: null, dataEnd: null, dataSize: null, data: null }; // | tag: vint | size: vint | data: Buffer(size) | this._tag_stack.push(tagObj); // <<<<<<<< // ポインタを進める this._cursor += tag.length; this._total += tag.length; // 読み込み状態変更 this._state = State.STATE_SIZE; return true; }; /** * vint された現在のタグの内容の大きさを読み込む * @return - return false when waiting for more data */ EBMLDecoder.prototype.readSize = function () { // tag.length が buffer の外にある if (this._cursor >= this._buffer.length) { return false; } // read ebml datasize vint without first byte var size = tools_1.readVint(this._buffer, this._cursor); // まだ読めない if (size == null) { return false; } // >>>>>>>>> // current tag の data size 決定 var tagObj = this._tag_stack[this._tag_stack.length - 1]; tagObj.sizeEnd = tagObj.sizeStart + size.length; tagObj.dataStart = tagObj.sizeEnd; tagObj.dataSize = size.value; if (size.value === -1) { // unknown size tagObj.dataEnd = -1; if (tagObj.type === "m") { tagObj.unknownSize = true; } } else { tagObj.dataEnd = tagObj.sizeEnd + size.value; } // <<<<<<<< // ポインタを進める this._cursor += size.length; this._total += size.length; this._state = State.STATE_CONTENT; return true; }; /** * データ読み込み */ EBMLDecoder.prototype.readContent = function () { var tagObj = this._tag_stack[this._tag_stack.length - 1]; // master element は子要素を持つので生データはない if (tagObj.type === 'm') { // console.log('content should be tags'); tagObj.isEnd = false; this._result.push(tagObj); this._state = State.STATE_TAG; // この Mastert Element は空要素か if (tagObj.dataSize === 0) { // 即座に終了タグを追加 var elm = Object.assign({}, tagObj, { isEnd: true }); this._result.push(elm); this._tag_stack.pop(); // スタックからこのタグを捨てる } return true; } // waiting for more data if (this._buffer.length < this._cursor + tagObj.dataSize) { return false; } // タグの中身の生データ var data = this._buffer.slice(this._cursor, this._cursor + tagObj.dataSize); // 読み終わったバッファを捨てて読み込んでいる部分のバッファのみ残す this._buffer = this._buffer.slice(this._cursor + tagObj.dataSize); tagObj.data = data; // >>>>>>>>> switch (tagObj.type) { //case "m": break; // Master-Element - contains other EBML sub-elements of the next lower level case "u": tagObj.value = data.readUIntBE(0, data.length); break; // Unsigned Integer - Big-endian, any size from 1 to 8 octets case "i": tagObj.value = data.readIntBE(0, data.length); break; // Signed Integer - Big-endian, any size from 1 to 8 octets case "f": tagObj.value = tagObj.dataSize === 4 ? data.readFloatBE(0) : tagObj.dataSize === 8 ? data.readDoubleBE(0) : (console.warn("cannot read " + tagObj.dataSize + " octets float. failback to 0"), 0); break; // Float - Big-endian, defined for 4 and 8 octets (32, 64 bits) case "s": tagObj.value = data.toString("ascii"); break; // ascii // Printable ASCII (0x20 to 0x7E), zero-padded when needed case "8": tagObj.value = data.toString("utf8"); break; // Unicode string, zero padded when needed (RFC 2279) case "b": tagObj.value = data; break; // Binary - not interpreted by the parser case "d": tagObj.value = tools_1.convertEBMLDateToJSDate(new int64_buffer_1.Int64BE(data).toNumber()); break; // nano second; Date.UTC(2001,1,1,0,0,0,0) === 980985600000 // Date - signed 8 octets integer in nanoseconds with 0 indicating // the precise beginning of the millennium (at 2001-01-01T00:00:00,000000000 UTC) } if (tagObj.value === null) { throw new Error("unknown tag type:" + tagObj.type); } this._result.push(tagObj); // <<<<<<<< // ポインタを進める this._total += tagObj.dataSize; // タグ待ちモードに変更 this._state = State.STATE_TAG; this._cursor = 0; this._tag_stack.pop(); // remove the object from the stack while (this._tag_stack.length > 0) { var topEle = this._tag_stack[this._tag_stack.length - 1]; // 親が不定長サイズなので閉じタグは期待できない if (topEle.dataEnd < 0) { this._tag_stack.pop(); // 親タグを捨てる return true; } // 閉じタグの来るべき場所まで来たかどうか if (this._total < topEle.dataEnd) { break; } // 閉じタグを挿入すべきタイミングが来た if (topEle.type !== "m") { throw new Error("parent element is not master element"); } var elm = Object.assign({}, topEle, { isEnd: true }); this._result.push(elm); this._tag_stack.pop(); } return true; }; return EBMLDecoder; }()); exports.default = EBMLDecoder; }, { "./tools": 5, "int64-buffer": 15, "matroska/lib/schema": 17 }], 2: [function (require, module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var tools = require("./tools"); var tools_1 = require("./tools"); var schema = require("matroska/lib/schema"); var byEbmlID = schema.byEbmlID; var EBMLEncoder = /** @class */ (function () { function EBMLEncoder() { this._schema = byEbmlID; this._buffers = []; this._stack = []; } EBMLEncoder.prototype.encode = function (elms) { var _this = this; return tools.concat(elms.reduce(function (lst, elm) { return lst.concat(_this.encodeChunk(elm)); }, [])).buffer; }; EBMLEncoder.prototype.encodeChunk = function (elm) { if (elm.type === "m") { if (!elm.isEnd) { this.startTag(elm); } else { this.endTag(elm); } } else { this.writeTag(elm); } return this.flush(); }; EBMLEncoder.prototype.flush = function () { var ret = this._buffers; this._buffers = []; return ret; }; EBMLEncoder.prototype.getSchemaInfo = function (tagName) { var tagNums = Object.keys(this._schema).map(Number); for (var i = 0; i < tagNums.length; i++) { var tagNum = tagNums[i]; if (this._schema[tagNum].name === tagName) { return new tools_1.Buffer(tagNum.toString(16), 'hex'); } } return null; }; EBMLEncoder.prototype.writeTag = function (elm) { var tagName = elm.name; var tagId = this.getSchemaInfo(tagName); var tagData = elm.data; if (tagId == null) { throw new Error('No schema entry found for ' + tagName); } var data = tools.encodeTag(tagId, tagData); /** * 親要素が閉じタグあり(isEnd)なら閉じタグが来るまで待つ(children queに入る) */ if (this._stack.length > 0) { var last = this._stack[this._stack.length - 1]; last.children.push({ tagId: tagId, elm: elm, children: [], data: data }); return; } this._buffers = this._buffers.concat(data); return; }; EBMLEncoder.prototype.startTag = function (elm) { var tagName = elm.name; var tagId = this.getSchemaInfo(tagName); if (tagId == null) { throw new Error('No schema entry found for ' + tagName); } /** * 閉じタグ不定長の場合はスタックに積まずに即時バッファに書き込む */ if (elm.unknownSize) { var data = tools.encodeTag(tagId, new tools_1.Buffer(0), elm.unknownSize); this._buffers = this._buffers.concat(data); return; } var tag = { tagId: tagId, elm: elm, children: [], data: null }; if (this._stack.length > 0) { this._stack[this._stack.length - 1].children.push(tag); } this._stack.push(tag); }; EBMLEncoder.prototype.endTag = function (elm) { var tagName = elm.name; var tag = this._stack.pop(); if (tag == null) { throw new Error("EBML structure is broken"); } if (tag.elm.name !== elm.name) { throw new Error("EBML structure is broken"); } var childTagDataBuffers = tag.children.reduce(function (lst, child) { if (child.data === null) { throw new Error("EBML structure is broken"); } return lst.concat(child.data); }, []); var childTagDataBuffer = tools.concat(childTagDataBuffers); if (tag.elm.type === "m") { tag.data = tools.encodeTag(tag.tagId, childTagDataBuffer, tag.elm.unknownSize); } else { tag.data = tools.encodeTag(tag.tagId, childTagDataBuffer); } if (this._stack.length < 1) { this._buffers = this._buffers.concat(tag.data); } }; return EBMLEncoder; }()); exports.default = EBMLEncoder; }, { "./tools": 5, "matroska/lib/schema": 17 }], 3: [function (require, module, exports) { "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); var events_1 = require("events"); var tools = require("./tools"); /** * This is an informal code for reference. * EBMLReader is a class for getting information to enable seeking Webm recorded by MediaRecorder. * So please do not use for regular WebM files. */ var EBMLReader = /** @class */ (function (_super) { __extends(EBMLReader, _super); function EBMLReader() { var _this = _super.call(this) || this; _this.logGroup = ""; _this.hasLoggingStarted = false; _this.metadataloaded = false; _this.chunks = []; _this.stack = []; _this.segmentOffset = 0; _this.last2SimpleBlockVideoTrackTimecode = [0, 0]; _this.last2SimpleBlockAudioTrackTimecode = [0, 0]; _this.lastClusterTimecode = 0; _this.lastClusterPosition = 0; _this.timecodeScale = 1000000; // webm default TimecodeScale is 1ms _this.metadataSize = 0; _this.metadatas = []; _this.cues = []; _this.firstVideoBlockRead = false; _this.firstAudioBlockRead = false; _this.currentTrack = { TrackNumber: -1, TrackType: -1, DefaultDuration: null, CodecDelay: null }; _this.trackTypes = []; _this.trackDefaultDuration = []; _this.trackCodecDelay = []; _this.trackInfo = { type: "nothing" }; _this.ended = false; _this.logging = false; _this.use_duration_every_simpleblock = false; _this.use_webp = false; _this.use_segment_info = true; _this.drop_default_duration = true; return _this; } /** * emit final state. */ EBMLReader.prototype.stop = function () { this.ended = true; this.emit_segment_info(); // clean up any unclosed Master Elements at the end of the stream. while (this.stack.length) { this.stack.pop(); if (this.logging) { console.groupEnd(); } } // close main group if set, logging is enabled, and has actually logged anything. if (this.logging && this.hasLoggingStarted && this.logGroup) { console.groupEnd(); } }; /** * emit chunk info */ EBMLReader.prototype.emit_segment_info = function () { var data = this.chunks; this.chunks = []; if (!this.metadataloaded) { this.metadataloaded = true; this.metadatas = data; var videoTrackNum = this.trackTypes.indexOf(1); // find first video track var audioTrackNum = this.trackTypes.indexOf(2); // find first audio track this.trackInfo = videoTrackNum >= 0 && audioTrackNum >= 0 ? { type: "both", trackNumber: videoTrackNum } : videoTrackNum >= 0 ? { type: "video", trackNumber: videoTrackNum } : audioTrackNum >= 0 ? { type: "audio", trackNumber: audioTrackNum } : { type: "nothing" }; if (!this.use_segment_info) { return; } this.emit("metadata", { data: data, metadataSize: this.metadataSize }); } else { if (!this.use_segment_info) { return; } var timecode = this.lastClusterTimecode; var duration = this.duration; var timecodeScale = this.timecodeScale; this.emit("cluster", { timecode: timecode, data: data }); this.emit("duration", { timecodeScale: timecodeScale, duration: duration }); } }; EBMLReader.prototype.read = function (elm) { var _this = this; var drop = false; if (this.ended) { // reader is finished return; } if (elm.type === "m") { // 閉じタグの自動挿入 if (elm.isEnd) { this.stack.pop(); } else { var parent_1 = this.stack[this.stack.length - 1]; if (parent_1 != null && parent_1.level >= elm.level) { // 閉じタグなしでレベルが下がったら閉じタグを挿入 this.stack.pop(); // From http://w3c.github.io/media-source/webm-byte-stream-format.html#webm-media-segments // This fixes logging for webm streams with Cluster of unknown length and no Cluster closing elements. if (this.logging) { console.groupEnd(); } parent_1.dataEnd = elm.dataEnd; parent_1.dataSize = elm.dataEnd - parent_1.dataStart; parent_1.unknownSize = false; var o = Object.assign({}, parent_1, { name: parent_1.name, type: parent_1.type, isEnd: true }); this.chunks.push(o); } this.stack.push(elm); } } if (elm.type === "m" && elm.name == "Segment") { if (this.segmentOffset != 0) { console.warn("Multiple segments detected!"); } this.segmentOffset = elm.dataStart; this.emit("segment_offset", this.segmentOffset); } else if (elm.type === "b" && elm.name === "SimpleBlock") { var _a = tools.ebmlBlock(elm.data), timecode = _a.timecode, trackNumber = _a.trackNumber, frames_1 = _a.frames; if (this.trackTypes[trackNumber] === 1) { // trackType === 1 => video track if (!this.firstVideoBlockRead) { this.firstVideoBlockRead = true; if (this.trackInfo.type === "both" || this.trackInfo.type === "video") { var CueTime = this.lastClusterTimecode + timecode; this.cues.push({ CueTrack: trackNumber, CueClusterPosition: this.lastClusterPosition, CueTime: CueTime }); this.emit("cue_info", { CueTrack: trackNumber, CueClusterPosition: this.lastClusterPosition, CueTime: this.lastClusterTimecode }); this.emit("cue", { CueTrack: trackNumber, CueClusterPosition: this.lastClusterPosition, CueTime: CueTime }); } } this.last2SimpleBlockVideoTrackTimecode = [this.last2SimpleBlockVideoTrackTimecode[1], timecode]; } else if (this.trackTypes[trackNumber] === 2) { // trackType === 2 => audio track if (!this.firstAudioBlockRead) { this.firstAudioBlockRead = true; if (this.trackInfo.type === "audio") { var CueTime = this.lastClusterTimecode + timecode; this.cues.push({ CueTrack: trackNumber, CueClusterPosition: this.lastClusterPosition, CueTime: CueTime }); this.emit("cue_info", { CueTrack: trackNumber, CueClusterPosition: this.lastClusterPosition, CueTime: this.lastClusterTimecode }); this.emit("cue", { CueTrack: trackNumber, CueClusterPosition: this.lastClusterPosition, CueTime: CueTime }); } } this.last2SimpleBlockAudioTrackTimecode = [this.last2SimpleBlockAudioTrackTimecode[1], timecode]; } if (this.use_duration_every_simpleblock) { this.emit("duration", { timecodeScale: this.timecodeScale, duration: this.duration }); } if (this.use_webp) { frames_1.forEach(function (frame) { var startcode = frame.slice(3, 6).toString("hex"); if (startcode !== "9d012a") { return; } ; // VP8 の場合 var webpBuf = tools.VP8BitStreamToRiffWebPBuffer(frame); var webp = new Blob([webpBuf], { type: "image/webp" }); var currentTime = _this.duration; _this.emit("webp", { currentTime: currentTime, webp: webp }); }); } } else if (elm.type === "m" && elm.name === "Cluster" && elm.isEnd === false) { this.firstVideoBlockRead = false; this.firstAudioBlockRead = false; this.emit_segment_info(); this.emit("cluster_ptr", elm.tagStart); this.lastClusterPosition = elm.tagStart; } else if (elm.type === "u" && elm.name === "Timecode") { this.lastClusterTimecode = elm.value; } else if (elm.type === "u" && elm.name === "TimecodeScale") { this.timecodeScale = elm.value; } else if (elm.type === "m" && elm.name === "TrackEntry") { if (elm.isEnd) { this.trackTypes[this.currentTrack.TrackNumber] = this.currentTrack.TrackType; this.trackDefaultDuration[this.currentTrack.TrackNumber] = this.currentTrack.DefaultDuration; this.trackCodecDelay[this.currentTrack.TrackNumber] = this.currentTrack.CodecDelay; } else { this.currentTrack = { TrackNumber: -1, TrackType: -1, DefaultDuration: null, CodecDelay: null }; } } else if (elm.type === "u" && elm.name === "TrackType") { this.currentTrack.TrackType = elm.value; } else if (elm.type === "u" && elm.name === "TrackNumber") { this.currentTrack.TrackNumber = elm.value; } else if (elm.type === "u" && elm.name === "CodecDelay") { this.currentTrack.CodecDelay = elm.value; } else if (elm.type === "u" && elm.name === "DefaultDuration") { // media source api は DefaultDuration を計算するとバグる。 // https://bugs.chromium.org/p/chromium/issues/detail?id=606000#c22 // chrome 58 ではこれを回避するために DefaultDuration 要素を抜き取った。 // chrome 58 以前でもこのタグを抜き取ることで回避できる if (this.drop_default_duration) { console.warn("DefaultDuration detected!, remove it"); drop = true; } else { this.currentTrack.DefaultDuration = elm.value; } } else if (elm.name === "unknown") { console.warn(elm); } if (!this.metadataloaded && elm.dataEnd > 0) { this.metadataSize = elm.dataEnd; } if (!drop) { this.chunks.push(elm); } if (this.logging) { this.put(elm); } }; Object.defineProperty(EBMLReader.prototype, "duration", { /** * DefaultDuration が定義されている場合は最後のフレームのdurationも考慮する * 単位 timecodeScale * * !!! if you need duration with seconds !!! * ```js * const nanosec = reader.duration * reader.timecodeScale; * const sec = nanosec / 1000 / 1000 / 1000; * ``` */ get: function () { if (this.trackInfo.type === "nothing") { console.warn("no video, no audio track"); return 0; } // defaultDuration は 生の nano sec var defaultDuration = 0; // nanoseconds var codecDelay = 0; var lastTimecode = 0; var _defaultDuration = this.trackDefaultDuration[this.trackInfo.trackNumber]; if (typeof _defaultDuration === "number") { defaultDuration = _defaultDuration; } else { // https://bugs.chromium.org/p/chromium/issues/detail?id=606000#c22 // default duration がないときに使う delta if (this.trackInfo.type === "both") { if (this.last2SimpleBlockAudioTrackTimecode[1] > this.last2SimpleBlockVideoTrackTimecode[1]) { // audio diff defaultDuration = (this.last2SimpleBlockAudioTrackTimecode[1] - this.last2SimpleBlockAudioTrackTimecode[0]) * this.timecodeScale; // audio delay var delay = this.trackCodecDelay[this.trackTypes.indexOf(2)]; // 2 => audio if (typeof delay === "number") { codecDelay = delay; } // audio timecode lastTimecode = this.last2SimpleBlockAudioTrackTimecode[1]; } else { // video diff defaultDuration = (this.last2SimpleBlockVideoTrackTimecode[1] - this.last2SimpleBlockVideoTrackTimecode[0]) * this.timecodeScale; // video delay var delay = this.trackCodecDelay[this.trackTypes.indexOf(1)]; // 1 => video if (typeof delay === "number") { codecDelay = delay; } // video timecode lastTimecode = this.last2SimpleBlockVideoTrackTimecode[1]; } } else if (this.trackInfo.type === "video") { defaultDuration = (this.last2SimpleBlockVideoTrackTimecode[1] - this.last2SimpleBlockVideoTrackTimecode[0]) * this.timecodeScale; var delay = this.trackCodecDelay[this.trackInfo.trackNumber]; // 2 => audio if (typeof delay === "number") { codecDelay = delay; } lastTimecode = this.last2SimpleBlockVideoTrackTimecode[1]; } else if (this.trackInfo.type === "audio") { defaultDuration = (this.last2SimpleBlockAudioTrackTimecode[1] - this.last2SimpleBlockAudioTrackTimecode[0]) * this.timecodeScale; var delay = this.trackCodecDelay[this.trackInfo.trackNumber]; // 1 => video if (typeof delay === "number") { codecDelay = delay; } lastTimecode = this.last2SimpleBlockAudioTrackTimecode[1]; } // else { not reached } } // convert to timecodescale var duration_nanosec = ((this.lastClusterTimecode + lastTimecode) * this.timecodeScale) + defaultDuration - codecDelay; var duration = duration_nanosec / this.timecodeScale; return Math.floor(duration); }, enumerable: true, configurable: true }); EBMLReader.prototype.addListener = function (event, listener) { return _super.prototype.addListener.call(this, event, listener); }; EBMLReader.prototype.put = function (elm) { if (!this.hasLoggingStarted) { this.hasLoggingStarted = true; if (this.logging && this.logGroup) { console.groupCollapsed(this.logGroup); } } if (elm.type === "m") { if (elm.isEnd) { console.groupEnd(); } else { console.group(elm.name + ":" + elm.tagStart); } } else if (elm.type === "b") { // for debug //if(elm.name === "SimpleBlock"){ //const o = EBML.tools.ebmlBlock(elm.value); //console.log(elm.name, elm.type, o.trackNumber, o.timecode); //}else{ console.log(elm.name, elm.type); //} } else { console.log(elm.name, elm.tagStart, elm.type, elm.value); } }; return EBMLReader; }(events_1.EventEmitter)); exports.default = EBMLReader; ; ; ; ; }, { "./tools": 5, "events": 13 }], 4: [function (require, module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var EBMLDecoder_1 = require("./EBMLDecoder"); exports.Decoder = EBMLDecoder_1.default; var EBMLEncoder_1 = require("./EBMLEncoder"); exports.Encoder = EBMLEncoder_1.default; var EBMLReader_1 = require("./EBMLReader"); exports.Reader = EBMLReader_1.default; var tools = require("./tools"); exports.tools = tools; var version = require("../package.json").version; exports.version = version; }, { "../package.json": 18, "./EBMLDecoder": 1, "./EBMLEncoder": 2, "./EBMLReader": 3, "./tools": 5 }], 5: [function (require, module, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /// <reference types="node"/> var int64_buffer_1 = require("int64-buffer"); var EBMLEncoder_1 = require("./EBMLEncoder"); var _Buffer = require("buffer/"); var _tools = require("ebml/lib/ebml/tools"); var _block = require("ebml-block"); exports.Buffer = _Buffer.Buffer; exports.readVint = _tools.readVint; exports.writeVint = _tools.writeVint; exports.ebmlBlock = _block; function readBlock(buf) { return exports.ebmlBlock(new exports.Buffer(buf)); } exports.readBlock = readBlock; /** * @param end - if end === false then length is unknown */ function encodeTag(tagId, tagData, unknownSize) { if (unknownSize === void 0) { unknownSize = false; } return concat([ tagId, unknownSize ? new exports.Buffer('01ffffffffffffff', 'hex') : exports.writeVint(tagData.length), tagData ]); } exports.encodeTag = encodeTag; /** * @return - SimpleBlock to WebP Filter */ function WebPFrameFilter(elms) { return WebPBlockFilter(elms).reduce(function (lst, elm) { var o = exports.ebmlBlock(elm.data); return o.frames.reduce(function (lst, frame) { // https://developers.Blob.com/speed/webp/docs/riff_container var webpBuf = VP8BitStreamToRiffWebPBuffer(frame); var webp = new Blob([webpBuf], { type: "image/webp" }); return lst.concat(webp); }, lst); }, []); } exports.WebPFrameFilter = WebPFrameFilter; /** * WebP ファイルにできる SimpleBlock の パスフィルタ */ function WebPBlockFilter(elms) { return elms.reduce(function (lst, elm) { if (elm.type !== "b") { return lst; } if (elm.name !== "SimpleBlock") { return lst; } var o = exports.ebmlBlock(elm.data); var hasWebP = o.frames.some(function (frame) { // https://tools.ietf.org/html/rfc6386#section-19.1 var startcode = frame.slice(3, 6).toString("hex"); return startcode === "9d012a"; }); if (!hasWebP) { return lst; } return lst.concat(elm); }, []); } exports.WebPBlockFilter = WebPBlockFilter; /** * @param frame - VP8 BitStream のうち startcode をもつ frame * @return - WebP ファイルの ArrayBuffer */ function VP8BitStreamToRiffWebPBuffer(frame) { var VP8Chunk = createRIFFChunk("VP8 ", frame); var WebPChunk = concat([ new exports.Buffer("WEBP", "ascii"), VP8Chunk ]); return createRIFFChunk("RIFF", WebPChunk); } exports.VP8BitStreamToRiffWebPBuffer = VP8BitStreamToRiffWebPBuffer; /** * RIFF データチャンクを作る */ function createRIFFChunk(FourCC, chunk) { var chunkSize = new exports.Buffer(4); chunkSize.writeUInt32LE(chunk.byteLength, 0); return concat([ new exports.Buffer(FourCC.substr(0, 4), "ascii"), chunkSize, chunk, new exports.Buffer(chunk.byteLength % 2 === 0 ? 0 : 1) // padding ]); } exports.createRIFFChunk = createRIFFChunk; /* Original Metadata m 0 EBML u 1 EBMLVersion 1 u 1 EBMLReadVersion 1 u 1 EBMLMaxIDLength 4 u 1 EBMLMaxSizeLength 8 s 1 DocType webm u 1 DocTypeVersion 4 u 1 DocTypeReadVersion 2 m 0 Segment m 1 Info segmentContentStartPos, all CueClusterPositions provided in info.cues will be relative to here and will need adjusted u 2 TimecodeScale 1000000 8 2 MuxingApp Chrome 8 2 WritingApp Chrome m 1 Tracks tracksStartPos m 2 TrackEntry u 3 TrackNumber 1 u 3 TrackUID 31790271978391090 u 3 TrackType 2 s 3 CodecID A_OPUS b 3 CodecPrivate <Buffer 19> m 3 Audio f 4 SamplingFrequency 48000 u 4 Channels 1 m 2 TrackEntry u 3 TrackNumber 2 u 3 TrackUID 24051277436254136 u 3 TrackType 1 s 3 CodecID V_VP8 m 3 Video u 4 PixelWidth 1024 u 4 PixelHeight 576 m 1 Cluster clusterStartPos u 2 Timecode 0 b 2 SimpleBlock track:2 timecode:0 keyframe:true invisible:false discardable:false lacing:1 */ /* Desired Metadata m 0 EBML u 1 EBMLVersion 1 u 1 EBMLReadVersion 1 u 1 EBMLMaxIDLength 4 u 1 EBMLMaxSizeLength 8 s 1 DocType webm u 1 DocTypeVersion 4 u 1 DocTypeReadVersion 2 m 0 Segment m 1 SeekHead -> This is SeekPosition 0, so all SeekPositions can be calculated as (bytePos - segmentContentStartPos), which is 44 in this case m 2 Seek b 3 SeekID -> Buffer([0x15, 0x49, 0xA9, 0x66]) Info u 3 SeekPosition -> infoStartPos = m 2 Seek b 3 SeekID -> Buffer([0x16, 0x54, 0xAE, 0x6B]) Tracks u 3 SeekPosition { tracksStartPos } m 2 Seek b 3 SeekID -> Buffer([0x1C, 0x53, 0xBB, 0x6B]) Cues u 3 SeekPosition { cuesStartPos } m 1 Info f 2 Duration 32480 -> overwrite, or insert if it doesn't exist u 2 TimecodeScale 1000000 8 2 MuxingApp Chrome 8 2 WritingApp Chrome m 1 Tracks m 2 TrackEntry u 3 TrackNumber 1 u 3 TrackUID 31790271978391090 u 3 TrackType 2 s 3 CodecID A_OPUS b 3 CodecPrivate <Buffer 19> m 3 Audio f 4 SamplingFrequency 48000 u 4 Channels 1 m 2 TrackEntry u 3 TrackNumber 2 u 3 TrackUID 24051277436254136 u 3 TrackType 1 s 3 CodecID V_VP8 m 3 Video u 4 PixelWidth 1024 u 4 PixelHeight 576 m 1 Cues -> cuesStartPos m 2 CuePoint u 3 CueTime 0 m 3 CueTrackPositions u 4 CueTrack 1 u 4 CueClusterPosition 3911 m 2 CuePoint u 3 CueTime 600 m 3 CueTrackPositions u 4 CueTrack 1 u 4 CueClusterPosition 3911 m 1 Cluster u 2 Timecode 0 b 2 SimpleBlock track:2 timecode:0 keyframe:true invisible:false discardable:false lacing:1 */ /** * convert the metadata from a streaming webm bytestream to a seekable file by inserting Duration, Seekhead and Cues * @param originalMetadata - orginal metadata (everything before the clusters start) from media recorder * @param duration - Duration (TimecodeScale) * @param cues - cue points for clusters */ function makeMetadataSeekable(originalMetadata, duration, cuesInfo) { // extract the header, we can reuse this as-is var header = extractElement("EBML", originalMetadata); var headerSize = encodedSizeOfEbml(header); //console.error("Header size: " + headerSize); //printElementIds(header); // After the header comes the Segment open tag, which in this implementation is always 12 bytes (4 byte id, 8 byte 'unknown length') // After that the segment content starts. All SeekPositions and CueClusterPosition must be relative to segmentContentStartPos var segmentContentStartPos = headerSize + 12; //console.error("segmentContentStartPos: " + segmentContentStartPos); // find the original metadata size, and adjust it for header size and Segment start element so we can keep all positions relative to segmentContentStartPos var originalMetadataSize = originalMetadata[originalMetadata.length - 1].dataEnd - segmentContentStartPos; //console.error("Original Metadata size: " + originalMetadataSize); //printElementIds(originalMetadata); // extract the segment info, remove the potentially existing Duration element, and add our own one. var info = extractElement("Info", originalMetadata); removeElement("Duration", info); info.splice(1, 0, { name: "Duration", type: "f", data: createFloatBuffer(duration, 8) }); var infoSize = encodedSizeOfEbml(info); //console.error("Info size: " + infoSize); //printElementIds(info); // extract the track info, we can re-use this as is var tracks = extractElement("Tracks", originalMetadata); var tracksSize = encodedSizeOfEbml(tracks); //console.error("Tracks size: " + tracksSize); //printElementIds(tracks); var seekHeadSize = 47; // Initial best guess, but could be slightly larger if the Cues element is huge. var seekHead = []; var cuesSize = 5 + cuesInfo.length * 15; // very rough initial approximation, depends a lot on file size and number of CuePoints var cues = []; var lastSizeDifference = -1; // // The size of SeekHead and Cues elements depends on how many bytes the offsets values can be encoded in. // The actual offsets in CueClusterPosition depend on the final size of the SeekHead and Cues elements // We need to iteratively converge to a stable solution. var maxIterations = 10; var _loop_1 = function (i) { // SeekHead starts at 0 var infoStart = seekHeadSize; // Info comes directly after SeekHead var tracksStart = infoStart + infoSize; // Tracks comes directly after Info var cuesStart = tracksStart + tracksSize; // Cues starts directly after var newMetadataSize = cuesStart + cuesSize; // total size of metadata // This is the offset all CueClusterPositions should be adjusted by due to the metadata size changing. var sizeDifference = newMetadataSize - originalMetadataSize; // console.error(`infoStart: ${infoStart}, infoSize: ${infoSize}`); // console.error(`tracksStart: ${tracksStart}, tracksSize: ${tracksSize}`); // console.error(`cuesStart: ${cuesStart}, cuesSize: ${cuesSize}`); // console.error(`originalMetadataSize: ${originalMetadataSize}, newMetadataSize: ${newMetadataSize}, sizeDifference: ${sizeDifference}`); // create the SeekHead element seekHead = []; seekHead.push({ name: "SeekHead", type: "m", isEnd: false }); seekHead.push({ name: "Seek", type: "m", isEnd: false }); seekHead.push({ name: "SeekID", type: "b", data: new exports.Buffer([0x15, 0x49, 0xA9, 0x66]) }); // Info seekHead.push({ name: "SeekPosition", type: "u", data: createUIntBuffer(infoStart) }); seekHead.push({ name: "Seek", type: "m", isEnd: true }); seekHead.push({ name: "Seek", type: "m", isEnd: false }); seekHead.push({ name: "SeekID", type: "b", data: new exports.Buffer([0x16, 0x54, 0xAE, 0x6B]) }); // Tracks seekHead.push({ name: "SeekPosition", type: "u", data: createUIntBuffer(tracksStart) }); seekHead.push({ name: "Seek", type: "m", isEnd: true }); seekHead.push({ name: "Seek", type: "m", isEnd: false }); seekHead.push({ name: "SeekID", type: "b", data: new exports.Buffer([0x1C, 0x53, 0xBB, 0x6B]) }); // Cues seekHead.push({ name: "SeekPosition", type: "u", data: createUIntBuffer(cuesStart) }); seekHead.push({ name: "Seek", type: "m", isEnd: true }); seekHead.push({ name: "SeekHead", type: "m", isEnd: true }); seekHeadSize = encodedSizeOfEbml(seekHead); //console.error("SeekHead size: " + seekHeadSize); //printElementIds(seekHead); // create the Cues element cues = []; cues.push({ name: "Cues", type: "m", isEnd: false }); cuesInfo.forEach(function (_a) { var CueTrack = _a.CueTrack, CueClusterPosition = _a.CueClusterPosition, CueTime = _a.CueTime; cues.push({ name: "CuePoint", type: "m", isEnd: false }); cues.push({ name: "CueTime", type: "u", data: createUIntBuffer(CueTime) }); cues.push({ name: "CueTrackPositions", type: "m", isEnd: false }); cues.push({ name: "CueTrack", type: "u", data: createUIntBuffer(CueTrack) }); //console.error(`CueClusterPosition: ${CueClusterPosition}, Corrected to: ${CueClusterPosition - segmentContentStartPos} , offset by ${sizeDifference} to become ${(CueClusterPosition - segmentContentStartPos) + sizeDifference - segmentContentStartPos}`); // EBMLReader returns CueClusterPosition with absolute byte offsets. The Cues section expects them as offsets from the first level 1 element of the Segment, so we need to adjust it. CueClusterPosition -= segmentContentStartPos; // We also need to adjust to take into account the change in metadata size from when EBMLReader read the original metadata. CueClusterPosition += sizeDifference; cues.push({ name: "CueClusterPosition", type: "u", data: createUIntBuffer(CueClusterPosition) }); cues.push({ name: "CueTrackPositions", type: "m", isEnd: true }); cues.push({ name: "CuePoint", type: "m", isEnd: true }); }); cues.push({ name: "Cues", type: "m", isEnd: true }); cuesSize = encodedSizeOfEbml(cues); //console.error("Cues size: " + cuesSize); //console.error("Cue count: " + cuesInfo.length); //printElementIds(cues); // If the new MetadataSize is not the same as the previous iteration, we need to run once more. if (lastSizeDifference !== sizeDifference) { lastSizeDifference = sizeDifference; if (i === maxIterations - 1) { throw new Error("Failed to converge to a stable metadata size"); } } else { return "break"; } }; for (var i = 0; i < maxIterations; i++) { var state_1 = _loop_1(i); if (state_1 === "break") break; } var finalMetadata = [].concat.apply([], [ header, { name: "Segment", type: "m", isEnd: false, unknownSize: true }, seekHead, info, tracks, cues ]); var result = new EBMLEncoder_1.default().encode(finalMetadata); //printElementIds(finalMetadata); //console.error(`Final metadata buffer size: ${result.byteLength}`); //console.error(`Final metadata buffer size without header and segment: ${result.byteLength-segmentContentStartPos}`); return result; } exports.makeMetadataSeekable = makeMetadataSeekable; /** * print all element id names in a list * @param metadata - array of EBML elements to print * export function printElementIds(metadata: EBML.EBMLElementBuffer[]) { let result: EBML.EBMLElementBuffer[] = []; let start: number = -1; for (let i = 0; i < metadata.length; i++) { console.error("\t id: " + metadata[i].name); } } */ /** * remove all occurances of an EBML element from an array of elements * If it's a MasterElement you will also remove the content. (everything between start and end) * @param idName - name of the EBML Element to remove. * @param metadata - array of EBML elements to search */ function removeElement(idName, metadata) { var result = []; var start = -1; for (var i = 0; i < metadata.length; i++) { var element = metadata[i]; if (element.name === idName) { // if it's a Master element, extract the start and end element, and everything in between if (element.type === "m") { if (!element.isEnd) { start = i; } else { // we've