UNPKG

@jxstjh/jhvideo

Version:

HTML5 jhvideo base on MPEG2-TS Stream Player

487 lines 21.8 kB
/* * Copyright (C) 2016 Bilibili. All Rights Reserved. * * @author zheng qian <xqq@xqq.im> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import EventEmitter from 'events'; import Log from '../utils/logger.js'; import Browser from '../utils/browser.js'; import MSEEvents from './mse-events.js'; import { SampleInfo, IDRSampleList } from './media-segment-info.js'; import { IllegalStateException } from '../utils/exception.js'; // Media Source Extensions controller var MSEController = /** @class */ (function () { function MSEController(config) { this.TAG = 'MSEController'; this._config = config; this._emitter = new EventEmitter(); if (this._config.isLive && this._config.autoCleanupSourceBuffer == undefined) { // For live stream, do auto cleanup by default this._config.autoCleanupSourceBuffer = true; } this.e = { onSourceOpen: this._onSourceOpen.bind(this), onSourceEnded: this._onSourceEnded.bind(this), onSourceClose: this._onSourceClose.bind(this), onSourceBufferError: this._onSourceBufferError.bind(this), onSourceBufferUpdateEnd: this._onSourceBufferUpdateEnd.bind(this) }; this._mediaSource = null; this._mediaSourceObjectURL = null; this._mediaElement = null; this._isBufferFull = false; this._hasPendingEos = false; this._requireSetMediaDuration = false; this._pendingMediaDuration = 0; this._pendingSourceBufferInit = []; this._mimeTypes = { video: null, audio: null }; this._sourceBuffers = { video: null, audio: null }; this._lastInitSegments = { video: null, audio: null }; this._pendingSegments = { video: [], audio: [] }; this._pendingRemoveRanges = { video: [], audio: [] }; this._idrList = new IDRSampleList(); } MSEController.prototype.destroy = function () { if (this._mediaElement || this._mediaSource) { this.detachMediaElement(); } this.e = null; this._emitter.removeAllListeners(); this._emitter = null; }; MSEController.prototype.on = function (event, listener) { this._emitter.addListener(event, listener); }; MSEController.prototype.off = function (event, listener) { this._emitter.removeListener(event, listener); }; MSEController.prototype.attachMediaElement = function (mediaElement) { if (this._mediaSource) { throw new IllegalStateException('MediaSource has been attached to an HTMLMediaElement!'); } var ms = this._mediaSource = new window.MediaSource(); ms.addEventListener('sourceopen', this.e.onSourceOpen); ms.addEventListener('sourceended', this.e.onSourceEnded); ms.addEventListener('sourceclose', this.e.onSourceClose); this._mediaElement = mediaElement; this._mediaSourceObjectURL = window.URL.createObjectURL(this._mediaSource); mediaElement.src = this._mediaSourceObjectURL; }; MSEController.prototype.detachMediaElement = function () { if (this._mediaSource) { var ms = this._mediaSource; for (var type in this._sourceBuffers) { // pending segments should be discard var ps = this._pendingSegments[type]; ps.splice(0, ps.length); this._pendingSegments[type] = null; this._pendingRemoveRanges[type] = null; this._lastInitSegments[type] = null; // remove all sourcebuffers var sb = this._sourceBuffers[type]; if (sb) { if (ms.readyState !== 'closed') { // ms edge can throw an error: Unexpected call to method or property access try { ms.removeSourceBuffer(sb); } catch (error) { Log.e(this.TAG, error.message); } sb.removeEventListener('error', this.e.onSourceBufferError); sb.removeEventListener('updateend', this.e.onSourceBufferUpdateEnd); } this._mimeTypes[type] = null; this._sourceBuffers[type] = null; } } if (ms.readyState === 'open') { try { ms.endOfStream(); } catch (error) { Log.e(this.TAG, error.message); } } ms.removeEventListener('sourceopen', this.e.onSourceOpen); ms.removeEventListener('sourceended', this.e.onSourceEnded); ms.removeEventListener('sourceclose', this.e.onSourceClose); this._pendingSourceBufferInit = []; this._isBufferFull = false; this._idrList.clear(); this._mediaSource = null; } if (this._mediaElement) { this._mediaElement.src = ''; this._mediaElement.removeAttribute('src'); this._mediaElement = null; } if (this._mediaSourceObjectURL) { window.URL.revokeObjectURL(this._mediaSourceObjectURL); this._mediaSourceObjectURL = null; } }; MSEController.prototype.appendInitSegment = function (initSegment, deferred) { if (!this._mediaSource || this._mediaSource.readyState !== 'open') { // sourcebuffer creation requires mediaSource.readyState === 'open' // so we defer the sourcebuffer creation, until sourceopen event triggered this._pendingSourceBufferInit.push(initSegment); // make sure that this InitSegment is in the front of pending segments queue this._pendingSegments[initSegment.type].push(initSegment); return; } var is = initSegment; var mimeType = "".concat(is.container); if (is.codec && is.codec.length > 0) { mimeType += ";codecs=".concat(is.codec); } var firstInitSegment = false; Log.v(this.TAG, 'Received Initialization Segment, mimeType: ' + mimeType); this._lastInitSegments[is.type] = is; if (mimeType !== this._mimeTypes[is.type]) { if (!this._mimeTypes[is.type]) { // empty, first chance create sourcebuffer firstInitSegment = true; try { var sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType); sb.addEventListener('error', this.e.onSourceBufferError); sb.addEventListener('updateend', this.e.onSourceBufferUpdateEnd); } catch (error) { Log.e(this.TAG, error.message); this._emitter.emit(MSEEvents.ERROR, { code: error.code, msg: error.message }); return; } } else { Log.v(this.TAG, "Notice: ".concat(is.type, " mimeType changed, origin: ").concat(this._mimeTypes[is.type], ", target: ").concat(mimeType)); } this._mimeTypes[is.type] = mimeType; } if (!deferred) { // deferred means this InitSegment has been pushed to pendingSegments queue this._pendingSegments[is.type].push(is); } if (!firstInitSegment) { // append immediately only if init segment in subsequence if (this._sourceBuffers[is.type] && !this._sourceBuffers[is.type].updating) { this._doAppendSegments(); } } if (Browser.safari && is.container === 'audio/mpeg' && is.mediaDuration > 0) { // 'audio/mpeg' track under Safari may cause MediaElement's duration to be NaN // Manually correct MediaSource.duration to make progress bar seekable, and report right duration this._requireSetMediaDuration = true; this._pendingMediaDuration = is.mediaDuration / 1000; // in seconds this._updateMediaSourceDuration(); } }; MSEController.prototype.appendMediaSegment = function (mediaSegment) { var ms = mediaSegment; this._pendingSegments[ms.type].push(ms); if (this._config.autoCleanupSourceBuffer && this._needCleanupSourceBuffer()) { this._doCleanupSourceBuffer(); } var sb = this._sourceBuffers[ms.type]; if (sb && !sb.updating && !this._hasPendingRemoveRanges()) { this._doAppendSegments(); } }; MSEController.prototype.seek = function (seconds) { // remove all appended buffers for (var type in this._sourceBuffers) { if (!this._sourceBuffers[type]) { continue; } // abort current buffer append algorithm var sb = this._sourceBuffers[type]; if (this._mediaSource.readyState === 'open') { try { // If range removal algorithm is running, InvalidStateError will be throwed // Ignore it. sb.abort(); } catch (error) { Log.e(this.TAG, error.message); } } // IDRList should be clear this._idrList.clear(); // pending segments should be discard var ps = this._pendingSegments[type]; ps.splice(0, ps.length); if (this._mediaSource.readyState === 'closed') { // Parent MediaSource object has been detached from HTMLMediaElement continue; } // record ranges to be remove from SourceBuffer for (var i = 0; i < sb.buffered.length; i++) { var start = sb.buffered.start(i); var end = sb.buffered.end(i); this._pendingRemoveRanges[type].push({ start: start, end: end }); } // if sb is not updating, let's remove ranges now! if (!sb.updating) { this._doRemoveRanges(); } // Safari 10 may get InvalidStateError in the later appendBuffer() after SourceBuffer.remove() call // Internal parser's state may be invalid at this time. Re-append last InitSegment to workaround. // Related issue: https://bugs.webkit.org/show_bug.cgi?id=159230 if (Browser.safari) { var lastInitSegment = this._lastInitSegments[type]; if (lastInitSegment) { this._pendingSegments[type].push(lastInitSegment); if (!sb.updating) { this._doAppendSegments(); } } } } }; MSEController.prototype.endOfStream = function () { var ms = this._mediaSource; var sb = this._sourceBuffers; if (!ms || ms.readyState !== 'open') { if (ms && ms.readyState === 'closed' && this._hasPendingSegments()) { // If MediaSource hasn't turned into open state, and there're pending segments // Mark pending endOfStream, defer call until all pending segments appended complete this._hasPendingEos = true; } return; } if (sb.video && sb.video.updating || sb.audio && sb.audio.updating) { // If any sourcebuffer is updating, defer endOfStream operation // See _onSourceBufferUpdateEnd() this._hasPendingEos = true; } else { this._hasPendingEos = false; // Notify media data loading complete // This is helpful for correcting total duration to match last media segment // Otherwise MediaElement's ended event may not be triggered ms.endOfStream(); } }; MSEController.prototype.getNearestKeyframe = function (dts) { return this._idrList.getLastSyncPointBeforeDts(dts); }; MSEController.prototype._needCleanupSourceBuffer = function () { if (!this._config.autoCleanupSourceBuffer) { return false; } var currentTime = this._mediaElement.currentTime; for (var type in this._sourceBuffers) { var sb = this._sourceBuffers[type]; if (sb) { var buffered = sb.buffered; if (buffered.length >= 1) { if (currentTime - buffered.start(0) >= this._config.autoCleanupMaxBackwardDuration) { return true; } } } } return false; }; MSEController.prototype._doCleanupSourceBuffer = function () { var currentTime = this._mediaElement.currentTime; for (var type in this._sourceBuffers) { var sb = this._sourceBuffers[type]; if (sb) { var buffered = sb.buffered; var doRemove = false; for (var i = 0; i < buffered.length; i++) { var start = buffered.start(i); var end = buffered.end(i); if (start <= currentTime && currentTime < end + 3) { // padding 3 seconds if (currentTime - start >= this._config.autoCleanupMaxBackwardDuration) { doRemove = true; var removeEnd = currentTime - this._config.autoCleanupMinBackwardDuration; this._pendingRemoveRanges[type].push({ start: start, end: removeEnd }); } } else if (end < currentTime) { doRemove = true; this._pendingRemoveRanges[type].push({ start: start, end: end }); } } if (doRemove && !sb.updating) { this._doRemoveRanges(); } } } }; MSEController.prototype._updateMediaSourceDuration = function () { var sb = this._sourceBuffers; if (this._mediaElement.readyState === 0 || this._mediaSource.readyState !== 'open') { return; } if ((sb.video && sb.video.updating) || (sb.audio && sb.audio.updating)) { return; } var current = this._mediaSource.duration; var target = this._pendingMediaDuration; if (target > 0 && (isNaN(current) || target > current)) { Log.v(this.TAG, "Update MediaSource duration from ".concat(current, " to ").concat(target)); this._mediaSource.duration = target; } this._requireSetMediaDuration = false; this._pendingMediaDuration = 0; }; MSEController.prototype._doRemoveRanges = function () { for (var type in this._pendingRemoveRanges) { if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) { continue; } var sb = this._sourceBuffers[type]; var ranges = this._pendingRemoveRanges[type]; while (ranges.length && !sb.updating) { var range = ranges.shift(); sb.remove(range.start, range.end); } } }; MSEController.prototype._doAppendSegments = function () { var pendingSegments = this._pendingSegments; for (var type in pendingSegments) { if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) { continue; } if (pendingSegments[type].length > 0) { var segment = pendingSegments[type].shift(); if (segment.timestampOffset) { // For MPEG audio stream in MSE, if unbuffered-seeking occurred // We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer. var currentOffset = this._sourceBuffers[type].timestampOffset; var targetOffset = segment.timestampOffset / 1000; // in seconds var delta = Math.abs(currentOffset - targetOffset); if (delta > 0.1) { // If time delta > 100ms Log.v(this.TAG, "Update MPEG audio timestampOffset from ".concat(currentOffset, " to ").concat(targetOffset)); this._sourceBuffers[type].timestampOffset = targetOffset; } delete segment.timestampOffset; } if (!segment.data || segment.data.byteLength === 0) { // Ignore empty buffer continue; } try { this._sourceBuffers[type].appendBuffer(segment.data); this._isBufferFull = false; if (type === 'video' && segment.hasOwnProperty('info')) { this._idrList.appendArray(segment.info.syncPoints); } } catch (error) { this._pendingSegments[type].unshift(segment); if (error.code === 22) { // QuotaExceededError /* Notice that FireFox may not throw QuotaExceededError if SourceBuffer is full * Currently we can only do lazy-load to avoid SourceBuffer become scattered. * SourceBuffer eviction policy may be changed in future version of FireFox. * * Related issues: * https://bugzilla.mozilla.org/show_bug.cgi?id=1279885 * https://bugzilla.mozilla.org/show_bug.cgi?id=1280023 */ // report buffer full, abort network IO if (!this._isBufferFull) { this._emitter.emit(MSEEvents.BUFFER_FULL); } this._isBufferFull = true; } else { Log.e(this.TAG, error.message); this._emitter.emit(MSEEvents.ERROR, { code: error.code, msg: error.message }); } } } } }; MSEController.prototype._onSourceOpen = function () { Log.v(this.TAG, 'MediaSource onSourceOpen'); this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen); // deferred sourcebuffer creation / initialization if (this._pendingSourceBufferInit.length > 0) { var pendings = this._pendingSourceBufferInit; while (pendings.length) { var segment = pendings.shift(); this.appendInitSegment(segment, true); } } // there may be some pending media segments, append them if (this._hasPendingSegments()) { this._doAppendSegments(); } this._emitter.emit(MSEEvents.SOURCE_OPEN); }; MSEController.prototype._onSourceEnded = function () { // fired on endOfStream Log.v(this.TAG, 'MediaSource onSourceEnded'); this._emitter.emit(MSEEvents.SOURCE_ENDED); }; MSEController.prototype._onSourceClose = function () { // fired on detaching from media element Log.v(this.TAG, 'MediaSource onSourceClose'); this._emitter.emit(MSEEvents.SOURCE_CLOSE); if (this._mediaSource && this.e != null) { this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen); this._mediaSource.removeEventListener('sourceended', this.e.onSourceEnded); this._mediaSource.removeEventListener('sourceclose', this.e.onSourceClose); } }; MSEController.prototype._hasPendingSegments = function () { var ps = this._pendingSegments; return ps.video.length > 0 || ps.audio.length > 0; }; MSEController.prototype._hasPendingRemoveRanges = function () { var prr = this._pendingRemoveRanges; return prr.video.length > 0 || prr.audio.length > 0; }; MSEController.prototype._onSourceBufferUpdateEnd = function () { if (this._requireSetMediaDuration) { this._updateMediaSourceDuration(); } else if (this._hasPendingRemoveRanges()) { this._doRemoveRanges(); } else if (this._hasPendingSegments()) { this._doAppendSegments(); } else if (this._hasPendingEos) { this.endOfStream(); } this._emitter.emit(MSEEvents.UPDATE_END); }; MSEController.prototype._onSourceBufferError = function (e) { Log.e(this.TAG, "SourceBuffer Error: ".concat(e)); // this error might not always be fatal, just ignore it }; return MSEController; }()); export default MSEController; //# sourceMappingURL=mse-controller.js.map