@jxstjh/jhvideo
Version:
HTML5 jhvideo base on MPEG2-TS Stream Player
438 lines • 20.5 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 MediaInfo from './media-info.js';
import FLVDemuxer from '../demux/flv-demuxer.js';
import TSDemuxer from '../demux/ts-demuxer';
import MP4Remuxer from '../remux/mp4-remuxer.js';
import DemuxErrors from '../demux/demux-errors.js';
import IOController from '../io/io-controller.js';
import TransmuxingEvents from './transmuxing-events.js';
import { LoaderStatus, LoaderErrors } from '../io/loader.js';
// Transmuxing (IO, Demuxing, Remuxing) controller, with multipart support
var TransmuxingController = /** @class */ (function () {
function TransmuxingController(mediaDataSource, config) {
this.TAG = 'TransmuxingController';
this._emitter = new EventEmitter();
this._config = config;
// treat single part media as multipart media, which has only one segment
if (!mediaDataSource.segments) {
mediaDataSource.segments = [{
duration: mediaDataSource.duration,
filesize: mediaDataSource.filesize,
url: mediaDataSource.url
}];
}
// fill in default IO params if not exists
if (typeof mediaDataSource.cors !== 'boolean') {
mediaDataSource.cors = true;
}
if (typeof mediaDataSource.withCredentials !== 'boolean') {
mediaDataSource.withCredentials = false;
}
this._mediaDataSource = mediaDataSource;
this._currentSegmentIndex = 0;
var totalDuration = 0;
this._mediaDataSource.segments.forEach(function (segment) {
// timestampBase for each segment, and calculate total duration
segment.timestampBase = totalDuration;
totalDuration += segment.duration;
// params needed by IOController
segment.cors = mediaDataSource.cors;
segment.withCredentials = mediaDataSource.withCredentials;
// referrer policy control, if exist
if (config.referrerPolicy) {
segment.referrerPolicy = config.referrerPolicy;
}
});
if (!isNaN(totalDuration) && this._mediaDataSource.duration !== totalDuration) {
this._mediaDataSource.duration = totalDuration;
}
this._mediaInfo = null;
this._demuxer = null;
this._remuxer = null;
this._ioctl = null;
this._pendingSeekTime = null;
this._pendingResolveSeekPoint = null;
this._statisticsReporter = null;
}
TransmuxingController.prototype.destroy = function () {
this._mediaInfo = null;
this._mediaDataSource = null;
if (this._statisticsReporter) {
this._disableStatisticsReporter();
}
if (this._ioctl) {
this._ioctl.destroy();
this._ioctl = null;
}
if (this._demuxer) {
this._demuxer.destroy();
this._demuxer = null;
}
if (this._remuxer) {
this._remuxer.destroy();
this._remuxer = null;
}
this._emitter.removeAllListeners();
this._emitter = null;
};
TransmuxingController.prototype.on = function (event, listener) {
this._emitter.addListener(event, listener);
};
TransmuxingController.prototype.off = function (event, listener) {
this._emitter.removeListener(event, listener);
};
TransmuxingController.prototype.start = function () {
this._loadSegment(0);
this._enableStatisticsReporter();
};
TransmuxingController.prototype._loadSegment = function (segmentIndex, optionalFrom) {
this._currentSegmentIndex = segmentIndex;
var dataSource = this._mediaDataSource.segments[segmentIndex];
var ioctl = this._ioctl = new IOController(dataSource, this._config, segmentIndex);
ioctl.onError = this._onIOException.bind(this);
ioctl.onSeeked = this._onIOSeeked.bind(this);
ioctl.onComplete = this._onIOComplete.bind(this);
ioctl.onRedirect = this._onIORedirect.bind(this);
ioctl.onRecoveredEarlyEof = this._onIORecoveredEarlyEof.bind(this);
ioctl.onInformation = this._onIOEInformation.bind(this);
if (optionalFrom) {
this._demuxer.bindDataSource(this._ioctl);
}
else {
ioctl.onDataArrival = this._onInitChunkArrival.bind(this);
}
ioctl.open(optionalFrom);
};
TransmuxingController.prototype.stop = function () {
this._internalAbort();
this._disableStatisticsReporter();
};
TransmuxingController.prototype._internalAbort = function () {
if (this._ioctl) {
this._ioctl.destroy();
this._ioctl = null;
}
};
TransmuxingController.prototype.pause = function () {
if (this._ioctl && this._ioctl.isWorking()) {
this._ioctl.pause();
this._disableStatisticsReporter();
}
};
TransmuxingController.prototype.resume = function () {
if (this._ioctl && this._ioctl.isPaused()) {
this._ioctl.resume();
this._enableStatisticsReporter();
}
};
TransmuxingController.prototype.seek = function (milliseconds) {
if (this._mediaInfo == null || !this._mediaInfo.isSeekable()) {
return;
}
var targetSegmentIndex = this._searchSegmentIndexContains(milliseconds);
if (targetSegmentIndex === this._currentSegmentIndex) {
// intra-segment seeking
var segmentInfo = this._mediaInfo.segments[targetSegmentIndex];
if (segmentInfo == undefined) {
// current segment loading started, but mediainfo hasn't received yet
// wait for the metadata loaded, then seek to expected position
this._pendingSeekTime = milliseconds;
}
else {
var keyframe = segmentInfo.getNearestKeyframe(milliseconds);
this._remuxer.seek(keyframe.milliseconds);
this._ioctl.seek(keyframe.fileposition);
// Will be resolved in _onRemuxerMediaSegmentArrival()
this._pendingResolveSeekPoint = keyframe.milliseconds;
}
}
else {
// cross-segment seeking
var targetSegmentInfo = this._mediaInfo.segments[targetSegmentIndex];
if (targetSegmentInfo == undefined) {
// target segment hasn't been loaded. We need metadata then seek to expected time
this._pendingSeekTime = milliseconds;
this._internalAbort();
this._remuxer.seek();
this._remuxer.insertDiscontinuity();
this._loadSegment(targetSegmentIndex);
// Here we wait for the metadata loaded, then seek to expected position
}
else {
// We have target segment's metadata, direct seek to target position
var keyframe = targetSegmentInfo.getNearestKeyframe(milliseconds);
this._internalAbort();
this._remuxer.seek(milliseconds);
this._remuxer.insertDiscontinuity();
this._demuxer.resetMediaInfo();
this._demuxer.timestampBase = this._mediaDataSource.segments[targetSegmentIndex].timestampBase;
this._loadSegment(targetSegmentIndex, keyframe.fileposition);
this._pendingResolveSeekPoint = keyframe.milliseconds;
this._reportSegmentMediaInfo(targetSegmentIndex);
}
}
this._enableStatisticsReporter();
};
TransmuxingController.prototype._searchSegmentIndexContains = function (milliseconds) {
var segments = this._mediaDataSource.segments;
var idx = segments.length - 1;
for (var i = 0; i < segments.length; i++) {
if (milliseconds < segments[i].timestampBase) {
idx = i - 1;
break;
}
}
return idx;
};
TransmuxingController.prototype._onInitChunkArrival = function (data, byteStart) {
var _this = this;
var probeData = null;
var consumed = 0;
if (byteStart > 0) {
// IOController seeked immediately after opened, byteStart > 0 callback may received
this._demuxer.bindDataSource(this._ioctl);
this._demuxer.timestampBase = this._mediaDataSource.segments[this._currentSegmentIndex].timestampBase;
consumed = this._demuxer.parseChunks(data, byteStart);
}
else if ((probeData = TSDemuxer.probe(data)).match) {
var demuxer = this._demuxer = new TSDemuxer(probeData, this._config);
if (!this._remuxer) {
this._remuxer = new MP4Remuxer(this._config);
}
demuxer.onError = this._onDemuxException.bind(this);
demuxer.onMediaInfo = this._onMediaInfo.bind(this);
demuxer.onMetaDataArrived = this._onMetaDataArrived.bind(this);
demuxer.onPESPrivateDataDescriptor = this._onPESPrivateDataDescriptor.bind(this);
demuxer.onPESPrivateData = this._onPESPrivateData.bind(this);
demuxer.onInformation = this._onPESInformationData.bind(this);
this._remuxer.bindDataSource(this._demuxer);
this._demuxer.bindDataSource(this._ioctl);
this._remuxer.onInitSegment = this._onRemuxerInitSegmentArrival.bind(this);
this._remuxer.onMediaSegment = this._onRemuxerMediaSegmentArrival.bind(this);
consumed = this._demuxer.parseChunks(data, byteStart);
}
else if ((probeData = FLVDemuxer.probe(data)).match) {
// Always create new FLVDemuxer
this._demuxer = new FLVDemuxer(probeData, this._config);
if (!this._remuxer) {
this._remuxer = new MP4Remuxer(this._config);
}
var mds = this._mediaDataSource;
if (mds.duration != undefined && !isNaN(mds.duration)) {
this._demuxer.overridedDuration = mds.duration;
}
if (typeof mds.hasAudio === 'boolean') {
this._demuxer.overridedHasAudio = mds.hasAudio;
}
if (typeof mds.hasVideo === 'boolean') {
this._demuxer.overridedHasVideo = mds.hasVideo;
}
this._demuxer.timestampBase = mds.segments[this._currentSegmentIndex].timestampBase;
this._demuxer.onError = this._onDemuxException.bind(this);
this._demuxer.onMediaInfo = this._onMediaInfo.bind(this);
this._demuxer.onMetaDataArrived = this._onMetaDataArrived.bind(this);
this._demuxer.onScriptDataArrived = this._onScriptDataArrived.bind(this);
this._demuxer.onScriptDataArrived = this._onScriptDataArrived.bind(this);
this._demuxer.onInformation = this._onPESInformationData.bind(this);
this._demuxer.onEsDataArrived = this._onEsDataArrived.bind(this);
this._demuxer.onEsScriptDataArrived = this._onEsDataArrived.bind(this);
this._remuxer.bindDataSource(this._demuxer
.bindDataSource(this._ioctl));
this._remuxer.onInitSegment = this._onRemuxerInitSegmentArrival.bind(this);
this._remuxer.onMediaSegment = this._onRemuxerMediaSegmentArrival.bind(this);
// console.log('0')
consumed = this._demuxer.parseChunks(data, byteStart);
// console.log('11')
}
else {
probeData = null;
Log.e(this.TAG, 'Non MPEG-TS/FLV, Unsupported media type!');
Promise.resolve().then(function () {
_this._internalAbort();
});
this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, DemuxErrors.FORMAT_UNSUPPORTED, 'Non MPEG-TS/FLV, Unsupported media type!');
consumed = 0;
}
return consumed;
};
TransmuxingController.prototype._onMediaInfo = function (mediaInfo) {
var _this = this;
if (this._mediaInfo == null) {
// Store first segment's mediainfo as global mediaInfo
this._mediaInfo = Object.assign({}, mediaInfo);
this._mediaInfo.keyframesIndex = null;
this._mediaInfo.segments = [];
this._mediaInfo.segmentCount = this._mediaDataSource.segments.length;
Object.setPrototypeOf(this._mediaInfo, MediaInfo.prototype);
}
var segmentInfo = Object.assign({}, mediaInfo);
Object.setPrototypeOf(segmentInfo, MediaInfo.prototype);
this._mediaInfo.segments[this._currentSegmentIndex] = segmentInfo;
// notify mediaInfo update
this._reportSegmentMediaInfo(this._currentSegmentIndex);
if (this._pendingSeekTime != null) {
Promise.resolve().then(function () {
var target = _this._pendingSeekTime;
_this._pendingSeekTime = null;
_this.seek(target);
});
}
};
TransmuxingController.prototype._onMetaDataArrived = function (metadata) {
this._emitter.emit(TransmuxingEvents.METADATA_ARRIVED, metadata);
};
TransmuxingController.prototype._onScriptDataArrived = function (data) {
this._emitter.emit(TransmuxingEvents.SCRIPTDATA_ARRIVED, data);
};
// h265
TransmuxingController.prototype._onEsDataArrived = function (e, t) {
var i = new Uint8Array(t.data);
t.data = i;
this._emitter.emit(TransmuxingEvents.ESDATA_ARRIVED, e, t);
};
TransmuxingController.prototype._onEsScriptDataArrived = function (e) {
var t = this;
this._emitter && Promise.resolve().then(function () {
t._emitter.emit(TransmuxingEvents.ESSCRIPTDATA_ARRIVED, e);
});
};
TransmuxingController.prototype._onPESPrivateDataDescriptor = function (descriptor) {
this._emitter.emit(TransmuxingEvents.PES_PRIVATE_DATA_DESCRIPTOR, descriptor);
};
TransmuxingController.prototype._onPESPrivateData = function (private_data) {
var timestamp_base = this._remuxer.getTimestampBase();
if (private_data.pts != undefined) {
private_data.pts -= timestamp_base;
}
if (private_data.nearest_pts != undefined) {
private_data.nearest_pts -= timestamp_base;
}
if (private_data.dts != undefined) {
private_data.dts -= timestamp_base;
}
this._emitter.emit(TransmuxingEvents.PES_PRIVATE_DATA_ARRIVED, private_data);
};
TransmuxingController.prototype._onIOSeeked = function () {
this._remuxer.insertDiscontinuity();
};
TransmuxingController.prototype._onIOComplete = function (extraData) {
var segmentIndex = extraData;
var nextSegmentIndex = segmentIndex + 1;
if (nextSegmentIndex < this._mediaDataSource.segments.length) {
this._internalAbort();
if (this._remuxer) {
this._remuxer.flushStashedSamples();
}
this._loadSegment(nextSegmentIndex);
}
else {
if (this._remuxer) {
this._remuxer.flushStashedSamples();
}
this._emitter.emit(TransmuxingEvents.LOADING_COMPLETE);
this._disableStatisticsReporter();
}
};
TransmuxingController.prototype._onIORedirect = function (redirectedURL) {
var segmentIndex = this._ioctl.extraData;
this._mediaDataSource.segments[segmentIndex].redirectedURL = redirectedURL;
};
TransmuxingController.prototype._onIORecoveredEarlyEof = function () {
this._emitter.emit(TransmuxingEvents.RECOVERED_EARLY_EOF);
};
TransmuxingController.prototype._onIOEInformation = function (type, info) {
this._emitter.emit(TransmuxingEvents.JSON_INFORMATION, type, info);
};
TransmuxingController.prototype._onIOException = function (type, info) {
console.log('info:', info);
// Log.e(this.TAG, `IOException: type = ${type}, code = ${info.code}, msg = ${info.msg}`);
this._emitter.emit(TransmuxingEvents.IO_ERROR, type, info);
if ((typeof (info) === 'string' && (info === 'mseSourceEnded' || info.indexOf('0x0'))) || !info) {
this._disableStatisticsReporter();
}
};
TransmuxingController.prototype._onDemuxException = function (type, info) {
Log.e(this.TAG, "DemuxException: type = ".concat(type, ", info = ").concat(info));
this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, type, info);
};
TransmuxingController.prototype._onPESInformationData = function (type, info) {
this._emitter.emit(TransmuxingEvents.JSON_INFORMATION, type, info);
};
TransmuxingController.prototype._onRemuxerInitSegmentArrival = function (type, initSegment) {
this._emitter.emit(TransmuxingEvents.INIT_SEGMENT, type, initSegment);
};
TransmuxingController.prototype._onRemuxerMediaSegmentArrival = function (type, mediaSegment) {
if (this._pendingSeekTime != null) {
// Media segments after new-segment cross-seeking should be dropped.
return;
}
this._emitter.emit(TransmuxingEvents.MEDIA_SEGMENT, type, mediaSegment);
// Resolve pending seekPoint
if (this._pendingResolveSeekPoint != null && type === 'video') {
var syncPoints = mediaSegment.info.syncPoints;
var seekpoint = this._pendingResolveSeekPoint;
this._pendingResolveSeekPoint = null;
// Safari: Pass PTS for recommend_seekpoint
if (Browser.safari && syncPoints.length > 0 && syncPoints[0].originalDts === seekpoint) {
seekpoint = syncPoints[0].pts;
}
// else: use original DTS (keyframe.milliseconds)
this._emitter.emit(TransmuxingEvents.RECOMMEND_SEEKPOINT, seekpoint);
}
};
TransmuxingController.prototype._enableStatisticsReporter = function () {
if (this._statisticsReporter == null) {
this._statisticsReporter = self.setInterval(this._reportStatisticsInfo.bind(this), this._config.statisticsInfoReportInterval);
}
};
TransmuxingController.prototype._disableStatisticsReporter = function () {
if (this._statisticsReporter) {
self.clearInterval(this._statisticsReporter);
this._statisticsReporter = null;
}
};
TransmuxingController.prototype._reportSegmentMediaInfo = function (segmentIndex) {
var segmentInfo = this._mediaInfo.segments[segmentIndex];
var exportInfo = Object.assign({}, segmentInfo);
exportInfo.duration = this._mediaInfo.duration;
exportInfo.segmentCount = this._mediaInfo.segmentCount;
delete exportInfo.segments;
delete exportInfo.keyframesIndex;
this._emitter.emit(TransmuxingEvents.MEDIA_INFO, exportInfo);
};
TransmuxingController.prototype._reportStatisticsInfo = function () {
var info = {};
info.url = this._ioctl.currentURL;
info.hasRedirect = this._ioctl.hasRedirect;
if (info.hasRedirect) {
info.redirectedURL = this._ioctl.currentRedirectedURL;
}
info.speed = this._ioctl.currentSpeed;
info.loaderType = this._ioctl.loaderType;
info.currentSegmentIndex = this._currentSegmentIndex;
info.totalSegmentCount = this._mediaDataSource.segments.length;
this._emitter.emit(TransmuxingEvents.STATISTICS_INFO, info);
};
return TransmuxingController;
}());
export default TransmuxingController;
//# sourceMappingURL=transmuxing-controller.js.map