UNPKG

mpegts.js

Version:

HTML5 MPEG2-TS Stream Player

582 lines (475 loc) 20.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 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'; import {LoaderStatus, LoaderErrors} from '../io/loader.js'; // Transmuxing (IO, Demuxing, Remuxing) controller, with multipart support class TransmuxingController { constructor(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; let totalDuration = 0; this._mediaDataSource.segments.forEach((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; } destroy() { 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; } on(event, listener) { this._emitter.addListener(event, listener); } off(event, listener) { this._emitter.removeListener(event, listener); } start() { this._loadSegment(0); this._enableStatisticsReporter(); } _loadSegment(segmentIndex, optionalFrom) { this._currentSegmentIndex = segmentIndex; let dataSource = this._mediaDataSource.segments[segmentIndex]; let 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); if (optionalFrom) { this._demuxer.bindDataSource(this._ioctl); } else { ioctl.onDataArrival = this._onInitChunkArrival.bind(this); } ioctl.open(optionalFrom); } stop() { this._internalAbort(); this._disableStatisticsReporter(); } _internalAbort() { if (this._ioctl) { this._ioctl.destroy(); this._ioctl = null; } } pause() { // take a rest if (this._ioctl && this._ioctl.isWorking()) { this._ioctl.pause(); this._disableStatisticsReporter(); } } resume() { if (this._ioctl && this._ioctl.isPaused()) { this._ioctl.resume(); this._enableStatisticsReporter(); } } seek(milliseconds) { if (this._mediaInfo == null || !this._mediaInfo.isSeekable()) { return; } let targetSegmentIndex = this._searchSegmentIndexContains(milliseconds); if (targetSegmentIndex === this._currentSegmentIndex) { // intra-segment seeking let 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 { let 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 let 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 let 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(); } _searchSegmentIndexContains(milliseconds) { let segments = this._mediaDataSource.segments; let idx = segments.length - 1; for (let i = 0; i < segments.length; i++) { if (milliseconds < segments[i].timestampBase) { idx = i - 1; break; } } return idx; } _onInitChunkArrival(data, byteStart) { let 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 { // byteStart == 0, Initial data, probe it first let probeData = null; // Try probing input data as FLV first probeData = FLVDemuxer.probe(data); if (probeData.match) { // Hit as FLV this._setupFLVDemuxerRemuxer(probeData); consumed = this._demuxer.parseChunks(data, byteStart); } if (!probeData.match && !probeData.needMoreData) { // Non-FLV, try MPEG-TS probe probeData = TSDemuxer.probe(data); if (probeData.match) { // Hit as MPEG-TS this._setupTSDemuxerRemuxer(probeData); consumed = this._demuxer.parseChunks(data, byteStart); } } if (!probeData.match && !probeData.needMoreData) { // Both probing as FLV / MPEG-TS failed, report error probeData = null; Log.e(this.TAG, 'Non MPEG-TS/FLV, Unsupported media type!'); Promise.resolve().then(() => { this._internalAbort(); }); this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, DemuxErrors.FORMAT_UNSUPPORTED, 'Non MPEG-TS/FLV, Unsupported media type!'); // Leave consumed as 0 } } return consumed; } _setupFLVDemuxerRemuxer(probeData) { this._demuxer = new FLVDemuxer(probeData, this._config); if (!this._remuxer) { this._remuxer = new MP4Remuxer(this._config); } let 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._remuxer.bindDataSource(this._demuxer .bindDataSource(this._ioctl )); this._remuxer.onInitSegment = this._onRemuxerInitSegmentArrival.bind(this); this._remuxer.onMediaSegment = this._onRemuxerMediaSegmentArrival.bind(this); } _setupTSDemuxerRemuxer(probeData) { let 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.onTimedID3Metadata = this._onTimedID3Metadata.bind(this); demuxer.onSynchronousKLVMetadata = this._onSynchronousKLVMetadata.bind(this); demuxer.onAsynchronousKLVMetadata = this._onAsynchronousKLVMetadata.bind(this); demuxer.onSMPTE2038Metadata = this._onSMPTE2038Metadata.bind(this); demuxer.onSCTE35Metadata = this._onSCTE35Metadata.bind(this); demuxer.onPESPrivateDataDescriptor = this._onPESPrivateDataDescriptor.bind(this); demuxer.onPESPrivateData = this._onPESPrivateData.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); } _onMediaInfo(mediaInfo) { 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); } let 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(() => { let target = this._pendingSeekTime; this._pendingSeekTime = null; this.seek(target); }); } } _onMetaDataArrived(metadata) { this._emitter.emit(TransmuxingEvents.METADATA_ARRIVED, metadata); } _onScriptDataArrived(data) { this._emitter.emit(TransmuxingEvents.SCRIPTDATA_ARRIVED, data); } _onTimedID3Metadata(timed_id3_metadata) { let timestamp_base = this._remuxer.getTimestampBase(); if (timestamp_base == undefined) { return; } if (timed_id3_metadata.pts != undefined) { timed_id3_metadata.pts -= timestamp_base; } if (timed_id3_metadata.dts != undefined) { timed_id3_metadata.dts -= timestamp_base; } this._emitter.emit(TransmuxingEvents.TIMED_ID3_METADATA_ARRIVED, timed_id3_metadata); } _onSynchronousKLVMetadata(synchronous_klv_metadata) { let timestamp_base = this._remuxer.getTimestampBase(); if (timestamp_base == undefined) { return; } if (synchronous_klv_metadata.pts != undefined) { synchronous_klv_metadata.pts -= timestamp_base; } if (synchronous_klv_metadata.dts != undefined) { synchronous_klv_metadata.dts -= timestamp_base; } this._emitter.emit(TransmuxingEvents.SYNCHRONOUS_KLV_METADATA_ARRIVED, synchronous_klv_metadata); } _onAsynchronousKLVMetadata(asynchronous_klv_metadata) { this._emitter.emit(TransmuxingEvents.ASYNCHRONOUS_KLV_METADATA_ARRIVED, asynchronous_klv_metadata); } _onSMPTE2038Metadata(smpte2038_metadata) { let timestamp_base = this._remuxer.getTimestampBase(); if (timestamp_base == undefined) { return; } if (smpte2038_metadata.pts != undefined) { smpte2038_metadata.pts -= timestamp_base; } if (smpte2038_metadata.dts != undefined) { smpte2038_metadata.dts -= timestamp_base; } if (smpte2038_metadata.nearest_pts != undefined) { smpte2038_metadata.nearest_pts -= timestamp_base; } this._emitter.emit(TransmuxingEvents.SMPTE2038_METADATA_ARRIVED, smpte2038_metadata); } _onSCTE35Metadata(scte35) { let timestamp_base = this._remuxer.getTimestampBase(); if (timestamp_base == undefined) { return; } if (scte35.pts != undefined) { scte35.pts -= timestamp_base; } if (scte35.nearest_pts != undefined) { scte35.nearest_pts -= timestamp_base; } this._emitter.emit(TransmuxingEvents.SCTE35_METADATA_ARRIVED, scte35); } _onPESPrivateDataDescriptor(descriptor) { this._emitter.emit(TransmuxingEvents.PES_PRIVATE_DATA_DESCRIPTOR, descriptor); } _onPESPrivateData(private_data) { let timestamp_base = this._remuxer.getTimestampBase(); if (timestamp_base == undefined) { return; } 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); } _onIOSeeked() { this._remuxer.insertDiscontinuity(); } _onIOComplete(extraData) { let segmentIndex = extraData; let 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(); } } _onIORedirect(redirectedURL) { let segmentIndex = this._ioctl.extraData; this._mediaDataSource.segments[segmentIndex].redirectedURL = redirectedURL; } _onIORecoveredEarlyEof() { this._emitter.emit(TransmuxingEvents.RECOVERED_EARLY_EOF); } _onIOException(type, info) { Log.e(this.TAG, `IOException: type = ${type}, code = ${info.code}, msg = ${info.msg}`); this._emitter.emit(TransmuxingEvents.IO_ERROR, type, info); this._disableStatisticsReporter(); } _onDemuxException(type, info) { Log.e(this.TAG, `DemuxException: type = ${type}, info = ${info}`); this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, type, info); } _onRemuxerInitSegmentArrival(type, initSegment) { this._emitter.emit(TransmuxingEvents.INIT_SEGMENT, type, initSegment); } _onRemuxerMediaSegmentArrival(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') { let syncPoints = mediaSegment.info.syncPoints; let 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); } } _enableStatisticsReporter() { if (this._statisticsReporter == null) { this._statisticsReporter = self.setInterval( this._reportStatisticsInfo.bind(this), this._config.statisticsInfoReportInterval); } } _disableStatisticsReporter() { if (this._statisticsReporter) { self.clearInterval(this._statisticsReporter); this._statisticsReporter = null; } } _reportSegmentMediaInfo(segmentIndex) { let segmentInfo = this._mediaInfo.segments[segmentIndex]; let 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); } _reportStatisticsInfo() { let 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); } } export default TransmuxingController;