UNPKG

@cycjimmy/jsmpeg-player

Version:
1,583 lines (1,264 loc) 216 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.JSMpeg = factory()); }(this, (function () { 'use strict'; function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function unwrapExports (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var isString = createCommonjsModule(function (module, exports) { Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; /** * determine a string type * @param str * @returns {boolean} */ var _default = function _default(str) { return typeof str === 'string' && str.constructor === String; }; exports["default"] = _default; }); var isString$1 = unwrapExports(isString); var isPromise = createCommonjsModule(function (module, exports) { Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; /** * determine a promise type * @param promise * @returns {boolean} */ var _default = function _default(promise) { return Object.prototype.toString.call(promise).slice(8, -1) === 'Promise'; }; exports["default"] = _default; }); unwrapExports(isPromise); var functionToPromise = createCommonjsModule(function (module, exports) { Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var _isPromise = _interopRequireDefault(isPromise); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } /** * function to promise * @param normalFunction * @param timeout * @returns {Promise<any>} */ var _default = function _default(normalFunction) { var timeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; if ((0, _isPromise["default"])(normalFunction)) { return normalFunction; } // eslint-disable-next-line no-undef return new Promise(function (resolve) { normalFunction(); setTimeout(resolve, timeout); }); }; exports["default"] = _default; }); var functionToPromise$1 = unwrapExports(functionToPromise); function styleInject(css, ref) { if (ref === void 0) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css_248z = "._2RIcqcDcdd,._9Rxgc3-sVf,.H-DEgIh9Aq,.nk-GvVcgZw{position:absolute;z-index:1;left:0;top:0;width:100%;height:100%}._9Rxgc3-sVf{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}._2RIcqcDcdd,.nk-GvVcgZw{display:block}._2RIcqcDcdd.HE6yLI4_vV{display:none}._9Rxgc3-sVf,.H-DEgIh9Aq{opacity:.7;cursor:pointer;-webkit-tap-highlight-color:rgba(255,0,0,0)}.HE6yLI4_vV._9Rxgc3-sVf,.HE6yLI4_vV.H-DEgIh9Aq{display:none}._9Rxgc3-sVf{z-index:10}._9Rxgc3-sVf>svg{width:12vw;height:12vw;max-width:60px;max-height:60px;fill:#fff}.H-DEgIh9Aq{z-index:10;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end}.H-DEgIh9Aq>svg{margin:0 15px 15px 0;width:9vw;height:9vw;max-width:40px;max-height:40px;fill:#fff}"; var _style = {"canvas":"nk-GvVcgZw","poster":"_2RIcqcDcdd","playButton":"_9Rxgc3-sVf","unmuteButton":"H-DEgIh9Aq","hidden":"HE6yLI4_vV"}; styleInject(css_248z); /** * PLAY_BUTTON HTML * @type {string} */ var PLAY_BUTTON = "\n<svg viewBox=\"0 0 64 64\">\n <path d=\"M26,45.5L44,32L26,18.6v27V45.5L26,45.5z M32,2C15.4,2,2,15.5,2,32c0,16.6,13.4,30,30,30c16.6,0,30-13.4,30-30 C62,15.4,48.5,2,32,2L32,2z M32,56c-9.7,0-18.5-5.9-22.2-14.8C6.1,32.2,8.1,21.9,15,15c6.9-6.9,17.2-8.9,26.2-5.2 C50.1,13.5,56,22.3,56,32C56,45.3,45.2,56,32,56L32,56z\"/>\n</svg>\n"; /** * UNMUTE_BUTTON HTML * @type {string} */ var UNMUTE_BUTTON = "\n<svg viewBox=\"0 0 64 64\">\n <path d=\"M58.3,45.5l-4.8-4.3c1.4-2.9,2.2-6.2,2.2-9.6c0-11.1-8.2-20.3-18.9-21.9V3.3C50.9,4.9,62,16.9,62,31.6 C62,36.6,60.6,41.4,58.3,45.5L58.3,45.5z M30.4,5.6v15.2l-8.3-7.3L30.4,5.6L30.4,5.6z M36.7,19.9c4.6,1.9,7.9,6.4,7.9,11.7 c0,0.6-0.1,1.1-0.1,1.7l-7.8-6.9V19.9L36.7,19.9z M57.5,60.7l-7.1-6.3c-3.9,2.9-8.6,4.8-13.7,5.4v-6.4c3.2-0.5,6.2-1.7,8.8-3.4 l-8.1-7.2c-0.2,0.1-0.5,0.3-0.7,0.4v-1l-6.3-5.6v20.2L15.4,42.6H2V20.5h10.2l-9.7-8.6l4.2-4.7L61.7,56L57.5,60.7L57.5,60.7z\"/>\n</svg>\n"; var VideoElement = /*#__PURE__*/function () { function VideoElement(wrapper, videoUrl, _temp, overlayOptions) { var _ref = _temp === void 0 ? {} : _temp, _ref$canvas = _ref.canvas, canvas = _ref$canvas === void 0 ? '' : _ref$canvas, _ref$poster = _ref.poster, poster = _ref$poster === void 0 ? '' : _ref$poster, _ref$autoplay = _ref.autoplay, autoplay = _ref$autoplay === void 0 ? false : _ref$autoplay, _ref$autoSetWrapperSi = _ref.autoSetWrapperSize, autoSetWrapperSize = _ref$autoSetWrapperSi === void 0 ? false : _ref$autoSetWrapperSi, _ref$loop = _ref.loop, loop = _ref$loop === void 0 ? false : _ref$loop, _ref$control = _ref.control, control = _ref$control === void 0 ? true : _ref$control, _ref$decodeFirstFrame = _ref.decodeFirstFrame, decodeFirstFrame = _ref$decodeFirstFrame === void 0 ? true : _ref$decodeFirstFrame, _ref$picMode = _ref.picMode, picMode = _ref$picMode === void 0 ? false : _ref$picMode, _ref$progressive = _ref.progressive, progressive = _ref$progressive === void 0 ? true : _ref$progressive, _ref$chunkSize = _ref.chunkSize, chunkSize = _ref$chunkSize === void 0 ? 1024 * 1024 : _ref$chunkSize, _ref$hooks = _ref.hooks, hooks = _ref$hooks === void 0 ? {} : _ref$hooks; if (overlayOptions === void 0) { overlayOptions = {}; } this.options = _extends({ videoUrl: videoUrl, canvas: canvas, poster: poster, picMode: picMode, autoplay: autoplay, autoSetWrapperSize: autoSetWrapperSize, loop: loop, control: control, decodeFirstFrame: decodeFirstFrame, progressive: progressive, chunkSize: chunkSize, hooks: _extends({ play: function play() {}, pause: function pause() {}, stop: function stop() {}, load: function load() {} }, hooks) }, overlayOptions); this.options.needPlayButton = this.options.control && !this.options.picMode; this.player = null; // Setup canvas and play button this.els = { wrapper: isString$1(wrapper) ? document.querySelector(wrapper) : wrapper, canvas: null, playButton: document.createElement('div'), unmuteButton: null, poster: null }; if (window.getComputedStyle(this.els.wrapper).getPropertyValue('position') === 'static') { this.els.wrapper.style.position = 'relative'; } this.els.wrapper.clientRect = this.els.wrapper.getBoundingClientRect(); this.initCanvas(); this.initPlayButton(); this.initPlayer(); } var _proto = VideoElement.prototype; _proto.initCanvas = function initCanvas() { if (this.options.canvas) { this.els.canvas = isString$1(this.options.canvas) ? document.querySelector(this.options.canvas) : this.options.canvas; } else { this.els.canvas = document.createElement('canvas'); this.els.canvas.classList.add(_style.canvas); this.els.wrapper.appendChild(this.els.canvas); } }; _proto.initPlayer = function initPlayer() { var _this = this; // Parse the data-options - we try to decode the values as json. This way // we can get proper boolean and number values. If JSON.parse() fails, // treat it as a string. this.options = _extends(this.options, { canvas: this.els.canvas }); // eslint-disable-next-line no-underscore-dangle var _options = _extends({}, this.options, { autoplay: false }); // Create the player instance this.player = new Player(this.options.videoUrl, _options, { play: function play() { if (_this.options.needPlayButton) { _this.els.playButton.classList.add(_style.hidden); } if (_this.els.poster) { _this.els.poster.classList.add(_style.hidden); } _this.options.hooks.play(); }, pause: function pause() { if (_this.options.needPlayButton) { _this.els.playButton.classList.remove(_style.hidden); } _this.options.hooks.pause(); }, stop: function stop() { if (_this.els.poster) { _this.els.poster.classList.remove(_style.hidden); } _this.options.hooks.stop(); }, load: function load() { if (_this.options.autoplay) { _this.play(); } _this._autoSetWrapperSize(); _this.options.hooks.load(); } }); this._copyPlayerFuncs(); this.els.wrapper.playerInstance = this.player; // Setup the poster element, if any if (this.options.poster && !this.options.autoplay && !this.player.options.streaming) { this.options.decodeFirstFrame = false; this.els.poster = new Image(); this.els.poster.src = this.options.poster; this.els.poster.classList.add(_style.poster); this.els.wrapper.appendChild(this.els.poster); } // Add the click handler if this video is pausable if (!this.player.options.streaming) { this.els.wrapper.addEventListener('click', this.onClick.bind(this)); } // Hide the play button if this video immediately begins playing if (this.options.autoplay || this.player.options.streaming) { this.els.playButton.classList.add(_style.hidden); } // Set up the unlock audio button for iOS devices. iOS only allows us to // play audio after a user action has initiated playing. For autoplay or // streaming players we set up a muted speaker icon as the button. For all // others, we can simply use the play button. if (this.player.audioOut && !this.player.audioOut.unlocked) { var unlockAudioElement = this.els.wrapper; if (this.options.autoplay || this.player.options.streaming) { this.els.unmuteButton = document.createElement('div'); this.els.unmuteButton.innerHTML = UNMUTE_BUTTON; this.els.unmuteButton.classList.add(_style.unmuteButton); this.els.wrapper.appendChild(this.els.unmuteButton); unlockAudioElement = this.els.unmuteButton; } this.unlockAudioBound = this.onUnlockAudio.bind(this, unlockAudioElement); unlockAudioElement.addEventListener('touchstart', this.unlockAudioBound, false); unlockAudioElement.addEventListener('click', this.unlockAudioBound, true); } }; _proto.initPlayButton = function initPlayButton() { if (!this.options.needPlayButton) { return; } this.els.playButton.classList.add(_style.playButton); this.els.playButton.innerHTML = PLAY_BUTTON; this.els.wrapper.appendChild(this.els.playButton); }; _proto._autoSetWrapperSize = function _autoSetWrapperSize() { var _this2 = this; if (!this.options.autoSetWrapperSize) { return Promise.resolve(); } var destination = this.player.video.destination; if (!destination) { return Promise.resolve(); } return Promise.resolve().then(function () { return functionToPromise$1(function () { _this2.els.wrapper.style.width = destination.width + "px"; _this2.els.wrapper.style.height = destination.height + "px"; }); }); }; _proto.onUnlockAudio = function onUnlockAudio(element, ev) { var _this3 = this; if (this.els.unmuteButton) { ev.preventDefault(); ev.stopPropagation(); } this.player.audioOut.unlock(function () { if (_this3.els.unmuteButton) { _this3.els.unmuteButton.classList.add(_style.hidden); } element.removeEventListener('touchstart', _this3.unlockAudioBound); element.removeEventListener('click', _this3.unlockAudioBound); }); }; _proto.onClick = function onClick() { if (!this.options.control) { return; } if (this.player.isPlaying) { this.pause(); } else { this.play(); } } /** * copy player functions * @private */ ; _proto._copyPlayerFuncs = function _copyPlayerFuncs() { var _this4 = this; this.play = function () { return _this4.player.play(); }; this.pause = function () { return _this4.player.pause(); }; this.stop = function () { return _this4.player.stop(); }; this.destroy = function () { _this4.player.destroy(); _this4.els.wrapper.innerHTML = ''; _this4.els.wrapper.playerInstance = null; }; }; return VideoElement; }(); var Now = function Now() { return window.performance ? window.performance.now() / 1000 : Date.now() / 1000; }; var CreateVideoElements = function CreateVideoElements() { var elements = document.querySelectorAll('.jsmpeg'); for (var i = 0; i < elements.length; i++) { // eslint-disable-next-line no-new new VideoElement(elements[i]); } }; var Fill = function Fill(array, value) { if (array.fill) { array.fill(value); } else { for (var i = 0; i < array.length; i++) { array[i] = value; } } }; var Base64ToArrayBuffer = function Base64ToArrayBuffer(base64) { var binary = window.atob(base64); var length = binary.length; var bytes = new Uint8Array(length); for (var i = 0; i < length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; }; /* eslint class-methods-use-this: ["error", { "exceptMethods": ["resume"] }] */ var AjaxSource = /*#__PURE__*/function () { function AjaxSource(url, options) { this.url = url; this.destination = null; this.request = null; this.streaming = false; this.completed = false; this.established = false; this.progress = 0; this.onEstablishedCallback = options.onSourceEstablished; this.onCompletedCallback = options.onSourceCompleted; if (options.hookOnEstablished) { this.hookOnEstablished = options.hookOnEstablished; } } var _proto = AjaxSource.prototype; _proto.connect = function connect(destination) { this.destination = destination; }; _proto.start = function start() { this.request = new XMLHttpRequest(); // eslint-disable-next-line func-names this.request.onreadystatechange = function () { if (this.request.readyState === this.request.DONE && this.request.status === 200) { this.onLoad(this.request.response); } }.bind(this); this.request.onprogress = this.onProgress.bind(this); this.request.open('GET', this.url); this.request.responseType = 'arraybuffer'; this.request.send(); }; _proto.resume = function resume() {// Nothing to do here }; _proto.destroy = function destroy() { this.request.abort(); }; _proto.onProgress = function onProgress(ev) { this.progress = ev.loaded / ev.total; }; _proto.onLoad = function onLoad(data) { this.established = true; this.completed = true; this.progress = 1; if (this.hookOnEstablished) { this.hookOnEstablished(); } if (this.onEstablishedCallback) { this.onEstablishedCallback(this); } if (this.onCompletedCallback) { this.onCompletedCallback(this); } if (this.destination) { this.destination.write(data); } }; return AjaxSource; }(); var AjaxProgressiveSource = /*#__PURE__*/function () { function AjaxProgressiveSource(url, options) { this.url = url; this.destination = null; this.request = null; this.streaming = false; this.completed = false; this.established = false; this.progress = 0; this.fileSize = 0; this.loadedSize = 0; this.chunkSize = options.chunkSize || 1024 * 1024; this.isLoading = false; this.loadStartTime = 0; this.throttled = options.throttled !== false; this.aborted = false; this.onEstablishedCallback = options.onSourceEstablished; this.onCompletedCallback = options.onSourceCompleted; if (options.hookOnEstablished) { this.hookOnEstablished = options.hookOnEstablished; } } var _proto = AjaxProgressiveSource.prototype; _proto.connect = function connect(destination) { this.destination = destination; }; _proto.start = function start() { var _this = this; this.request = new XMLHttpRequest(); this.request.onreadystatechange = function () { if (_this.request.readyState === _this.request.DONE) { _this.fileSize = parseInt(_this.request.getResponseHeader('Content-Length'), 10); _this.loadNextChunk(); } }; this.request.onprogress = this.onProgress.bind(this); this.request.open('HEAD', this.url); this.request.send(); }; _proto.resume = function resume(secondsHeadroom) { if (this.isLoading || !this.throttled) { return; } // Guess the worst case loading time with lots of safety margin. This is // somewhat arbitrary... var worstCaseLoadingTime = this.loadTime * 8 + 2; if (worstCaseLoadingTime > secondsHeadroom) { this.loadNextChunk(); } }; _proto.destroy = function destroy() { this.request.abort(); this.aborted = true; }; _proto.loadNextChunk = function loadNextChunk() { var _this2 = this; var start = this.loadedSize; var end = Math.min(this.loadedSize + this.chunkSize - 1, this.fileSize - 1); if (start >= this.fileSize || this.aborted) { this.completed = true; if (this.onCompletedCallback) { this.onCompletedCallback(this); } return; } this.isLoading = true; this.loadStartTime = Now(); this.request = new XMLHttpRequest(); this.request.onreadystatechange = function () { if (_this2.request.readyState === _this2.request.DONE && _this2.request.status >= 200 && _this2.request.status < 300) { _this2.onChunkLoad(_this2.request.response); } else if (_this2.request.readyState === _this2.request.DONE) { // Retry? if (_this2.loadFails++ < 3) { _this2.loadNextChunk(); } } }; if (start === 0) { this.request.onprogress = this.onProgress.bind(this); } this.request.open('GET', this.url + "?" + start + "-" + end); this.request.setRequestHeader('Range', "bytes=" + start + "-" + end); this.request.responseType = 'arraybuffer'; this.request.send(); }; _proto.onProgress = function onProgress(ev) { this.progress = ev.loaded / ev.total; }; _proto.onChunkLoad = function onChunkLoad(data) { var isFirstChunk = !this.established; this.established = true; this.progress = 1; this.loadedSize += data.byteLength; this.loadFails = 0; this.isLoading = false; if (isFirstChunk && this.hookOnEstablished) { this.hookOnEstablished(); } if (isFirstChunk && this.onEstablishedCallback) { this.onEstablishedCallback(this); } if (this.destination) { this.destination.write(data); } this.loadTime = Now() - this.loadStartTime; if (!this.throttled) { this.loadNextChunk(); } }; return AjaxProgressiveSource; }(); var WSSource = /*#__PURE__*/function () { function WSSource(url, options) { this.url = url; this.options = options; this.socket = null; this.streaming = true; this.callbacks = { connect: [], data: [] }; this.destination = null; this.reconnectInterval = // eslint-disable-next-line no-undefined options.reconnectInterval !== undefined ? options.reconnectInterval : 5; this.shouldAttemptReconnect = !!this.reconnectInterval; this.completed = false; this.established = false; this.progress = 0; this.reconnectTimeoutId = 0; this.onEstablishedCallback = options.onSourceEstablished; this.onCompletedCallback = options.onSourceCompleted; // Never used if (options.hookOnEstablished) { this.hookOnEstablished = options.hookOnEstablished; } } var _proto = WSSource.prototype; _proto.connect = function connect(destination) { this.destination = destination; }; _proto.destroy = function destroy() { clearTimeout(this.reconnectTimeoutId); this.shouldAttemptReconnect = false; this.socket.close(); }; _proto.start = function start() { this.shouldAttemptReconnect = !!this.reconnectInterval; this.progress = 0; this.established = false; this.socket = new WebSocket(this.url, this.options.protocols || null); this.socket.binaryType = 'arraybuffer'; this.socket.onmessage = this.onMessage.bind(this); this.socket.onopen = this.onOpen.bind(this); this.socket.onerror = this.onClose.bind(this); this.socket.onclose = this.onClose.bind(this); } // eslint-disable-next-line class-methods-use-this ; _proto.resume = function resume() {// Nothing to do here }; _proto.onOpen = function onOpen() { this.progress = 1; }; _proto.onClose = function onClose() { var _this = this; if (this.shouldAttemptReconnect) { clearTimeout(this.reconnectTimeoutId); this.reconnectTimeoutId = setTimeout(function () { _this.start(); }, this.reconnectInterval * 1000); } }; _proto.onMessage = function onMessage(ev) { var isFirstChunk = !this.established; this.established = true; if (isFirstChunk && this.hookOnEstablished) { this.hookOnEstablished(); } if (isFirstChunk && this.onEstablishedCallback) { this.onEstablishedCallback(this); } if (this.destination) { this.destination.write(ev.data); } }; return WSSource; }(); var BitBuffer = /*#__PURE__*/function () { function BitBuffer(bufferOrLength, mode) { if (typeof bufferOrLength === 'object') { this.bytes = bufferOrLength instanceof Uint8Array ? bufferOrLength : new Uint8Array(bufferOrLength); this.byteLength = this.bytes.length; } else { this.bytes = new Uint8Array(bufferOrLength || 1024 * 1024); this.byteLength = 0; } this.mode = mode || BitBuffer.MODE.EXPAND; this.index = 0; } var _proto = BitBuffer.prototype; _proto.resize = function resize(size) { var newBytes = new Uint8Array(size); if (this.byteLength !== 0) { this.byteLength = Math.min(this.byteLength, size); newBytes.set(this.bytes, 0, this.byteLength); } this.bytes = newBytes; this.index = Math.min(this.index, this.byteLength << 3); }; _proto.evict = function evict(sizeNeeded) { var bytePos = this.index >> 3; var available = this.bytes.length - this.byteLength; // If the current index is the write position, we can simply reset both // to 0. Also reset (and throw away yet unread data) if we won't be able // to fit the new data in even after a normal eviction. if (this.index === this.byteLength << 3 || sizeNeeded > available + bytePos // emergency evac ) { this.byteLength = 0; this.index = 0; return; } else if (bytePos === 0) { // Nothing read yet - we can't evict anything return; } // Some browsers don't support copyWithin() yet - we may have to do // it manually using set and a subarray if (this.bytes.copyWithin) { this.bytes.copyWithin(0, bytePos, this.byteLength); } else { this.bytes.set(this.bytes.subarray(bytePos, this.byteLength)); } this.byteLength -= bytePos; this.index -= bytePos << 3; }; _proto.write = function write(buffers) { var isArrayOfBuffers = typeof buffers[0] === 'object'; var totalLength = 0; var available = this.bytes.length - this.byteLength; // Calculate total byte length if (isArrayOfBuffers) { totalLength = 0; for (var i = 0; i < buffers.length; i++) { totalLength += buffers[i].byteLength; } } else { totalLength = buffers.byteLength; } // Do we need to resize or evict? if (totalLength > available) { if (this.mode === BitBuffer.MODE.EXPAND) { var newSize = Math.max(this.bytes.length * 2, totalLength - available); this.resize(newSize); } else { this.evict(totalLength); } } if (isArrayOfBuffers) { for (var _i = 0; _i < buffers.length; _i++) { this.appendSingleBuffer(buffers[_i]); } } else { this.appendSingleBuffer(buffers); } return totalLength; }; _proto.appendSingleBuffer = function appendSingleBuffer(buffer) { buffer = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); this.bytes.set(buffer, this.byteLength); this.byteLength += buffer.length; }; _proto.findNextStartCode = function findNextStartCode() { for (var i = this.index + 7 >> 3; i < this.byteLength; i++) { if (this.bytes[i] === 0x00 && this.bytes[i + 1] === 0x00 && this.bytes[i + 2] === 0x01) { this.index = i + 4 << 3; return this.bytes[i + 3]; } } this.index = this.byteLength << 3; return -1; }; _proto.findStartCode = function findStartCode(code) { var current = this.findNextStartCode(); if (current === code || current === -1) { return current; } return -1; }; _proto.nextBytesAreStartCode = function nextBytesAreStartCode() { var i = this.index + 7 >> 3; return i >= this.byteLength || this.bytes[i] === 0x00 && this.bytes[i + 1] === 0x00 && this.bytes[i + 2] === 0x01; }; _proto.peek = function peek(count) { var offset = this.index; var value = 0; while (count) { var currentByte = this.bytes[offset >> 3]; var remaining = 8 - (offset & 7); // remaining bits in byte var read = remaining < count ? remaining : count; // bits in this run var shift = remaining - read; var mask = 0xff >> 8 - read; value = value << read | (currentByte & mask << shift) >> shift; offset += read; count -= read; } return value; }; _proto.read = function read(count) { var value = this.peek(count); this.index += count; return value; }; _proto.skip = function skip(count) { return this.index += count; }; _proto.rewind = function rewind(count) { this.index = Math.max(this.index - count, 0); }; _proto.has = function has(count) { return (this.byteLength << 3) - this.index >= count; }; return BitBuffer; }(); BitBuffer.MODE = { EVICT: 1, EXPAND: 2 }; var TS = /*#__PURE__*/function () { function TS() { this.bits = null; this.leftoverBytes = null; this.guessVideoFrameEnd = true; this.pidsToStreamIds = {}; this.pesPacketInfo = {}; this.startTime = 0; this.currentTime = 0; } var _proto = TS.prototype; _proto.connect = function connect(streamId, destination) { this.pesPacketInfo[streamId] = { destination: destination, currentLength: 0, totalLength: 0, pts: 0, buffers: [] }; }; _proto.write = function write(buffer) { if (this.leftoverBytes) { var totalLength = buffer.byteLength + this.leftoverBytes.byteLength; this.bits = new BitBuffer(totalLength); this.bits.write([this.leftoverBytes, buffer]); } else { this.bits = new BitBuffer(buffer); } // eslint-disable-next-line no-empty while (this.bits.has(188 << 3) && this.parsePacket()) {} var leftoverCount = this.bits.byteLength - (this.bits.index >> 3); this.leftoverBytes = leftoverCount > 0 ? this.bits.bytes.subarray(this.bits.index >> 3) : null; }; _proto.parsePacket = function parsePacket() { // Check if we're in sync with packet boundaries; attempt to resync if not. if (this.bits.read(8) !== 0x47) { if (!this.resync()) { // Couldn't resync; maybe next time... return false; } } var end = (this.bits.index >> 3) + 187; // eslint-disable-next-line no-unused-vars var transportError = this.bits.read(1); var payloadStart = this.bits.read(1); // eslint-disable-next-line no-unused-vars var transportPriority = this.bits.read(1); var pid = this.bits.read(13); // eslint-disable-next-line no-unused-vars var transportScrambling = this.bits.read(2); var adaptationField = this.bits.read(2); // eslint-disable-next-line no-unused-vars var continuityCounter = this.bits.read(4); // If this is the start of a new payload; signal the end of the previous // frame, if we didn't do so already. var streamId = this.pidsToStreamIds[pid]; if (payloadStart && streamId) { var pi = this.pesPacketInfo[streamId]; if (pi && pi.currentLength) { this.packetComplete(pi); } } // Extract current payload if (adaptationField & 0x1) { if (adaptationField & 0x2) { var adaptationFieldLength = this.bits.read(8); this.bits.skip(adaptationFieldLength << 3); } if (payloadStart && this.bits.nextBytesAreStartCode()) { this.bits.skip(24); streamId = this.bits.read(8); this.pidsToStreamIds[pid] = streamId; var packetLength = this.bits.read(16); this.bits.skip(8); var ptsDtsFlag = this.bits.read(2); this.bits.skip(6); var headerLength = this.bits.read(8); var payloadBeginIndex = this.bits.index + (headerLength << 3); var _pi = this.pesPacketInfo[streamId]; if (_pi) { var pts = 0; if (ptsDtsFlag & 0x2) { // The Presentation Timestamp is encoded as 33(!) bit // integer, but has a "marker bit" inserted at weird places // in between, making the whole thing 5 bytes in size. // You can't make this shit up... this.bits.skip(4); var p32_30 = this.bits.read(3); this.bits.skip(1); var p29_15 = this.bits.read(15); this.bits.skip(1); var p14_0 = this.bits.read(15); this.bits.skip(1); // Can't use bit shifts here; we need 33 bits of precision, // so we're using JavaScript's double number type. Also // divide by the 90khz clock to get the pts in seconds. pts = (p32_30 * 1073741824 + p29_15 * 32768 + p14_0) / 90000; this.currentTime = pts; if (this.startTime === -1) { this.startTime = pts; } } var payloadLength = packetLength ? packetLength - headerLength - 3 : 0; this.packetStart(_pi, pts, payloadLength); } // Skip the rest of the header without parsing it this.bits.index = payloadBeginIndex; } if (streamId) { // Attempt to detect if the PES packet is complete. For Audio (and // other) packets, we received a total packet length with the PES // header, so we can check the current length. // For Video packets, we have to guess the end by detecting if this // TS packet was padded - there's no good reason to pad a TS packet // in between, but it might just fit exactly. If this fails, we can // only wait for the next PES header for that stream. var _pi2 = this.pesPacketInfo[streamId]; if (_pi2) { var start = this.bits.index >> 3; var complete = this.packetAddData(_pi2, start, end); var hasPadding = !payloadStart && adaptationField & 0x2; if (complete || this.guessVideoFrameEnd && hasPadding) { this.packetComplete(_pi2); } } } } this.bits.index = end << 3; return true; }; _proto.resync = function resync() { // Check if we have enough data to attempt a resync. We need 5 full packets. if (!this.bits.has(188 * 6 << 3)) { return false; } var byteIndex = this.bits.index >> 3; // Look for the first sync token in the first 187 bytes for (var i = 0; i < 187; i++) { if (this.bits.bytes[byteIndex + i] === 0x47) { // Look for 4 more sync tokens, each 188 bytes appart var foundSync = true; for (var j = 1; j < 5; j++) { if (this.bits.bytes[byteIndex + i + 188 * j] !== 0x47) { foundSync = false; break; } } if (foundSync) { this.bits.index = byteIndex + i + 1 << 3; return true; } } } // In theory, we shouldn't arrive here. If we do, we had enough data but // still didn't find sync - this can only happen if we were fed garbage // data. Check your source! console.warn('JSMpeg: Possible garbage data. Skipping.'); this.bits.skip(187 << 3); return false; } // eslint-disable-next-line class-methods-use-this ; _proto.packetStart = function packetStart(pi, pts, payloadLength) { pi.totalLength = payloadLength; pi.currentLength = 0; pi.pts = pts; }; _proto.packetAddData = function packetAddData(pi, start, end) { pi.buffers.push(this.bits.bytes.subarray(start, end)); pi.currentLength += end - start; return pi.totalLength !== 0 && pi.currentLength >= pi.totalLength; } // eslint-disable-next-line class-methods-use-this ; _proto.packetComplete = function packetComplete(pi) { pi.destination.write(pi.pts, pi.buffers); pi.totalLength = 0; pi.currentLength = 0; pi.buffers = []; }; return TS; }(); TS.STREAM = { PACK_HEADER: 0xba, SYSTEM_HEADER: 0xbb, PROGRAM_MAP: 0xbc, PRIVATE_1: 0xbd, PADDING: 0xbe, PRIVATE_2: 0xbf, AUDIO_1: 0xc0, VIDEO_1: 0xe0, DIRECTORY: 0xff }; /* eslint class-methods-use-this: ["error", { "exceptMethods": ["destroy"] }] */ var BaseDecoder = /*#__PURE__*/function () { function BaseDecoder(options) { this.destination = null; this.canPlay = false; this.collectTimestamps = !options.streaming; this.bytesWritten = 0; this.timestamps = []; this.timestampIndex = 0; this.startTime = 0; this.decodedTime = 0; Object.defineProperty(this, 'currentTime', { get: this.getCurrentTime }); } var _proto = BaseDecoder.prototype; _proto.destroy = function destroy() {}; _proto.connect = function connect(destination) { this.destination = destination; }; _proto.bufferGetIndex = function bufferGetIndex() { return this.bits.index; }; _proto.bufferSetIndex = function bufferSetIndex(index) { this.bits.index = index; }; _proto.bufferWrite = function bufferWrite(buffers) { return this.bits.write(buffers); }; _proto.write = function write(pts, buffers) { if (this.collectTimestamps) { if (this.timestamps.length === 0) { this.startTime = pts; this.decodedTime = pts; } this.timestamps.push({ index: this.bytesWritten << 3, time: pts }); } this.bytesWritten += this.bufferWrite(buffers); this.canPlay = true; }; _proto.seek = function seek(time) { if (!this.collectTimestamps) { return; } this.timestampIndex = 0; for (var i = 0; i < this.timestamps.length; i++) { if (this.timestamps[i].time > time) { break; } this.timestampIndex = i; } var ts = this.timestamps[this.timestampIndex]; if (ts) { this.bufferSetIndex(ts.index); this.decodedTime = ts.time; } else { this.bits.index = 0; this.decodedTime = this.startTime; } }; _proto.decode = function decode() { this.advanceDecodedTime(0); }; _proto.advanceDecodedTime = function advanceDecodedTime(seconds) { if (this.collectTimestamps) { var newTimestampIndex = -1; var currentIndex = this.bufferGetIndex(); for (var i = this.timestampIndex; i < this.timestamps.length; i++) { if (this.timestamps[i].index > currentIndex) { break; } newTimestampIndex = i; } // Did we find a new PTS, different from the last? If so, we don't have // to advance the decoded time manually and can instead sync it exactly // to the PTS. if (newTimestampIndex !== -1 && newTimestampIndex !== this.timestampIndex) { this.timestampIndex = newTimestampIndex; this.decodedTime = this.timestamps[this.timestampIndex].time; return; } } this.decodedTime += seconds; }; _proto.getCurrentTime = function getCurrentTime() { return this.decodedTime; }; return BaseDecoder; }(); var MPEG1 = /*#__PURE__*/function (_BaseDecoder) { _inheritsLoose(MPEG1, _BaseDecoder); function MPEG1(options) { var _this; _this = _BaseDecoder.call(this, options) || this; _this.onDecodeCallback = options.onVideoDecode; var bufferSize = options.videoBufferSize || 512 * 1024; var bufferMode = options.streaming ? BitBuffer.MODE.EVICT : BitBuffer.MODE.EXPAND; _this.bits = new BitBuffer(bufferSize, bufferMode); _this.customIntraQuantMatrix = new Uint8Array(64); _this.customNonIntraQuantMatrix = new Uint8Array(64); _this.blockData = new Int32Array(64); _this.currentFrame = 0; _this.decodeFirstFrame = options.decodeFirstFrame !== false; return _this; } // eslint-disable-next-line consistent-return var _proto = MPEG1.prototype; _proto.write = function write(pts, buffers) { BaseDecoder.prototype.write.call(this, pts, buffers); if (!this.hasSequenceHeader) { if (this.bits.findStartCode(MPEG1.START.SEQUENCE) === -1) { return false; } this.decodeSequenceHeader(); if (this.decodeFirstFrame) { this.decode(); } } }; _proto.decode = function decode() { var startTime = Now(); if (!this.hasSequenceHeader) { return false; } if (this.bits.findStartCode(MPEG1.START.PICTURE) === -1) { return false; } this.decodePicture(); this.advanceDecodedTime(1 / this.frameRate); var elapsedTime = Now() - startTime; if (this.onDecodeCallback) { this.onDecodeCallback(this, elapsedTime); } return true; }; _proto.readHuffman = function readHuffman(codeTable) { var state = 0; do { state = codeTable[state + this.bits.read(1)]; } while (state >= 0 && codeTable[state] !== 0); return codeTable[state + 2]; }; _proto.decodeSequenceHeader = function decodeSequenceHeader() { var newWidth = this.bits.read(12); var newHeight = this.bits.read(12); // skip pixel aspect ratio this.bits.skip(4); this.frameRate = MPEG1.PICTURE_RATE[this.bits.read(4)]; // skip bitRate, marker, bufferSize and constrained bit this.bits.skip(18 + 1 + 10 + 1); if (newWidth !== this.width || newHeight !== this.height) { this.width = newWidth; this.height = newHeight; this.initBuffers(); if (this.destination) { this.destination.resize(newWidth, newHeight); } } if (this.bits.read(1)) { // load custom intra quant matrix? for (var i = 0; i < 64; i++) { this.customIntraQuantMatrix[MPEG1.ZIG_ZAG[i]] = this.bits.read(8); } this.intraQuantMatrix = this.customIntraQuantMatrix; } if (this.bits.read(1)) { // load custom non intra quant matrix? for (var _i = 0; _i < 64; _i++) { var idx = MPEG1.ZIG_ZAG[_i]; this.customNonIntraQuantMatrix[idx] = this.bits.read(8); } this.nonIntraQuantMatrix = this.customNonIntraQuantMatrix; } this.hasSequenceHeader = true; }; _proto.initBuffers = function initBuffers() { this.intraQuantMatrix = MPEG1.DEFAULT_INTRA_QUANT_MATRIX; this.nonIntraQuantMatrix = MPEG1.DEFAULT_NON_INTRA_QUANT_MATRIX; this.mbWidth = this.width + 15 >> 4; this.mbHeight = this.height + 15 >> 4; this.mbSize = this.mbWidth * this.mbHeight; this.codedWidth = this.mbWidth << 4; this.codedHeight = this.mbHeight << 4; this.codedSize = this.codedWidth * this.codedHeight; this.halfWidth = this.mbWidth << 3; this.halfHeight = this.mbHeight << 3; // Allocated buffers and resize the canvas this.currentY = new Uint8ClampedArray(this.codedSize); this.currentY32 = new Uint32Array(this.currentY.buffer); this.currentCr = new Uint8ClampedArray(this.codedSize >> 2); this.currentCr32 = new Uint32Array(this.currentCr.buffer); this.currentCb = new Uint8ClampedArray(this.codedSize >> 2); this.currentCb32 = new Uint32Array(this.currentCb.buffer); this.forwardY = new Uint8ClampedArray(this.codedSize); this.forwardY32 = new Uint32Array(this.forwardY.buffer); this.forwardCr = new Uint8ClampedArray(this.codedSize >> 2); this.forwardCr32 = new Uint32Array(this.forwardCr.buffer); this.forwardCb = new Uint8ClampedArray(this.codedSize >> 2); this.forwardCb32 = new Uint32Array(this.forwardCb.buffer); }; _proto.decodePicture = function decodePicture() { this.currentFrame++; this.bits.skip(10); // skip temporalReference this.pictureType = this.bits.read(3); this.bits.skip(16); // skip vbv_delay // Skip B and D frames or unknown coding type if (this.pictureType <= 0 || this.pictureType >= MPEG1.PICTURE_TYPE.B) { return; } // full_pel_forward, forward_f_code if (this.pictureType === MPEG1.PICTURE_TYPE.PREDICTIVE) { this.fullPelForward = this.bits.read(1); this.forwardFCode = this.bits.read(3); if (this.forwardFCode === 0) { // Ignore picture with zero forward_f_code return; } this.forwardRSize = this.forwardFCode - 1; this.forwardF = 1 << this.forwardRSize; } var code = 0; do { code = this.bits.findNextStartCode(); } while (code === MPEG1.START.EXTENSION || code === MPEG1.START.USER_DATA); while (code >= MPEG1.START.SLICE_FIRST && code <= MPEG1.START.SLICE_LAST) { this.decodeSlice(code & 0x000000ff); code = this.bits.findNextStartCode(); } if (code !== -1) { // We found the next start code; rewind 32bits and let the main loop // handle it. this.bits.rewind(32); } // Invoke decode callbacks if (this.destination) { this.destination.render(this.currentY, this.currentCr, this.currentCb, true); } // If this is a reference picutre then rotate the prediction pointers if (this.pictureType === MPEG1.PICTURE_TYPE.INTRA || this.pictureType === MPEG1.PICTURE_TYPE.PREDICTIVE) { var tmpY = this.forwardY; var tmpY32 = this.forwardY32; var tmpCr = this.forwardCr; var tmpCr32 = this.forwardCr32; var tmpCb = this.forwardCb; var tmpCb32 = this.forwardCb32; this.forwardY = this.currentY; this.forwardY32 = this.currentY32; this.forwardCr = this.currentCr; this.forwardCr32 = this.currentCr32; this.forwardCb = this.currentCb; this.forwardCb32 = this.currentCb32; this.currentY = tmpY; this.currentY32 = tmpY32; this.currentCr = tmpCr; this.currentCr32 = tmpCr32; this.currentCb = tmpCb; this.currentCb32 = tmpCb32; } }; _proto.decodeSlice = function decodeSlice(slice) { this.sliceBegin = true; this.macroblockAddress = (slice - 1) * this.mbWidth - 1; // Reset motion vectors and DC predictors this.motionFwH = this.motionFwHPrev = 0; this.motionFwV = this.motionFwVPrev = 0; this.dcPredictorY = 128; this.dcPredictorCr = 128; this.dcPredictorCb = 128; this.quantizerScale = this.bits.read(5); // skip extra bits while (this.bits.read(1)) { this.bits.skip(8); } do { this.decodeMacroblock(); } while (!this.bits.nextBytesAreStartCode()); }; _proto.decodeMacroblock = function decodeMacroblock() { // Decode macroblock_address_increment var increment = 0; var t = this.readHuffman(MPEG1.MACROBLOCK_ADDRESS_INCREMENT); while (t === 34) { // macroblock_stuffing t = this.readHuffman(MPEG1.MACROBLOCK_ADDRESS_INCREMENT); } while (t === 35) { // macroblock_escape increment += 33; t = this.readHuffman(MPEG1.MACROBLOCK_ADDRESS_INCREMENT); } increment += t; // Process any skipped macroblocks if (this.sliceBegin) { // The first macroblock_address_increment of each slice is relative // to beginning of the preverious row, not the preverious macroblock this.sliceBegin = false; this.macroblockAddress += increment; } else { if (this.macroblockAddress + increment >= this.mbSize) { // Illegal (too large) macroblock_address_increment return; } if (increment > 1) { // Skipped macroblocks reset DC predictors this.dcPredictorY = 128; this.dcPredictorCr = 128; this.dcPredictorCb = 128; // Skipped macroblocks in P-pictures reset motion vectors if (this.pictureType === MPEG1.PICTURE_TYPE.PREDICTIVE) { this.motionFwH = this.motionFwHPrev = 0; this.motionFwV = this.motionFwVPrev = 0; } } // Predict skipped macroblocks while (increment > 1) { this.macroblockAddress++; this.mbRow = this.macroblockAddress / this.mbWidth | 0; this.mbCol = this.macroblockAddress % this.mbWidth; this.copyMacroblock(this.motionFwH, this.motionFwV, this.forwardY, this.forwardCr, this.forwardCb); increment--; } this.macroblockAddress++; } this.mbRow = this.macroblockAddress / this.mbWidth | 0; this.mbCol = this.macroblockAddress % this.mbWidth; // Process the current macroblock var mbTable = MPEG1.MACROBLOCK_TYPE[this.pictureType]; this.macroblockType = this.readHuffman(mbTable); this.macroblockIntra = this.macroblockType & 0x01; this.macroblockMotFw = this.macroblockType & 0x08; // Quantizer scale if ((this.macroblockType & 0x10) !== 0) { this.quantizerScale = this.bits.read(5); } if (this.macroblockIntra) { // Intra-coded macroblocks reset motion vectors this.motionFwH = this.motionFwHPrev = 0; this.motionFwV = this.motionFwVPrev = 0; } else { // Non-intra macroblocks reset DC predictors this.dcPredictorY = 128;