@jxstjh/jhvideo
Version:
HTML5 jhvideo base on MPEG2-TS Stream Player
487 lines • 21.8 kB
JavaScript
/*
* 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