UNPKG

@jxstjh/jhvideo

Version:

HTML5 jhvideo base on MPEG2-TS Stream Player

438 lines 20.5 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 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