| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- /*
- * 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 PlayerEvents from './player-events.js';
- import Transmuxer from '../core/transmuxer.js';
- import TransmuxingEvents from '../core/transmuxing-events.js';
- import MSEController from '../core/mse-controller.js';
- import MSEEvents from '../core/mse-events.js';
- import {ErrorTypes, ErrorDetails} from './player-errors.js';
- import {createDefaultConfig} from '../config.js';
- import {InvalidArgumentException, IllegalStateException} from '../utils/exception.js';
- class FlvPlayer {
- constructor(mediaDataSource, config) {
- this.TAG = 'FlvPlayer';
- this._type = 'FlvPlayer';
- this._emitter = new EventEmitter();
- this._config = createDefaultConfig();
- if (typeof config === 'object') {
- Object.assign(this._config, config);
- }
- if (mediaDataSource.type.toLowerCase() !== 'flv') {
- throw new InvalidArgumentException('FlvPlayer requires an flv MediaDataSource input!');
- }
- if (mediaDataSource.isLive === true) {
- this._config.isLive = true;
- }
- this.e = {
- onvLoadedMetadata: this._onvLoadedMetadata.bind(this),
- onvSeeking: this._onvSeeking.bind(this),
- onvCanPlay: this._onvCanPlay.bind(this),
- onvStalled: this._onvStalled.bind(this),
- onvProgress: this._onvProgress.bind(this)
- };
- if (self.performance && self.performance.now) {
- this._now = self.performance.now.bind(self.performance);
- } else {
- this._now = Date.now;
- }
- this._pendingSeekTime = null; // in seconds
- this._requestSetTime = false;
- this._seekpointRecord = null;
- this._progressChecker = null;
- this._mediaDataSource = mediaDataSource;
- this._mediaElement = null;
- this._msectl = null;
- this._transmuxer = null;
- this._mseSourceOpened = false;
- this._hasPendingLoad = false;
- this._receivedCanPlay = false;
- this._mediaInfo = null;
- this._statisticsInfo = null;
- let chromeNeedIDRFix = (Browser.chrome &&
- (Browser.version.major < 50 ||
- (Browser.version.major === 50 && Browser.version.build < 2661)));
- this._alwaysSeekKeyframe = (chromeNeedIDRFix || Browser.msedge || Browser.msie) ? true : false;
- if (this._alwaysSeekKeyframe) {
- this._config.accurateSeek = false;
- }
- }
- destroy() {
- if (this._progressChecker != null) {
- window.clearInterval(this._progressChecker);
- this._progressChecker = null;
- }
- if (this._transmuxer) {
- this.unload();
- }
- if (this._mediaElement) {
- this.detachMediaElement();
- }
- this.e = null;
- this._mediaDataSource = null;
- this._emitter.removeAllListeners();
- this._emitter = null;
- }
- on(event, listener) {
- if (event === PlayerEvents.MEDIA_INFO) {
- if (this._mediaInfo != null) {
- Promise.resolve().then(() => {
- this._emitter.emit(PlayerEvents.MEDIA_INFO, this.mediaInfo);
- });
- }
- } else if (event === PlayerEvents.STATISTICS_INFO) {
- if (this._statisticsInfo != null) {
- Promise.resolve().then(() => {
- this._emitter.emit(PlayerEvents.STATISTICS_INFO, this.statisticsInfo);
- });
- }
- }
- this._emitter.addListener(event, listener);
- }
- off(event, listener) {
- this._emitter.removeListener(event, listener);
- }
- attachMediaElement(mediaElement) {
- this._mediaElement = mediaElement;
- mediaElement.addEventListener('loadedmetadata', this.e.onvLoadedMetadata);
- mediaElement.addEventListener('seeking', this.e.onvSeeking);
- mediaElement.addEventListener('canplay', this.e.onvCanPlay);
- mediaElement.addEventListener('stalled', this.e.onvStalled);
- mediaElement.addEventListener('progress', this.e.onvProgress);
- this._msectl = new MSEController(this._config);
- this._msectl.on(MSEEvents.UPDATE_END, this._onmseUpdateEnd.bind(this));
- this._msectl.on(MSEEvents.BUFFER_FULL, this._onmseBufferFull.bind(this));
- this._msectl.on(MSEEvents.SOURCE_OPEN, () => {
- this._mseSourceOpened = true;
- if (this._hasPendingLoad) {
- this._hasPendingLoad = false;
- this.load();
- }
- });
- this._msectl.on(MSEEvents.ERROR, (info) => {
- this._emitter.emit(PlayerEvents.ERROR,
- ErrorTypes.MEDIA_ERROR,
- ErrorDetails.MEDIA_MSE_ERROR,
- info
- );
- });
- this._msectl.attachMediaElement(mediaElement);
- if (this._pendingSeekTime != null) {
- try {
- mediaElement.currentTime = this._pendingSeekTime;
- this._pendingSeekTime = null;
- } catch (e) {
- // IE11 may throw InvalidStateError if readyState === 0
- // We can defer set currentTime operation after loadedmetadata
- }
- }
- }
- detachMediaElement() {
- if (this._mediaElement) {
- this._msectl.detachMediaElement();
- this._mediaElement.removeEventListener('loadedmetadata', this.e.onvLoadedMetadata);
- this._mediaElement.removeEventListener('seeking', this.e.onvSeeking);
- this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay);
- this._mediaElement.removeEventListener('stalled', this.e.onvStalled);
- this._mediaElement.removeEventListener('progress', this.e.onvProgress);
- this._mediaElement = null;
- }
- if (this._msectl) {
- this._msectl.destroy();
- this._msectl = null;
- }
- }
- load() {
- if (!this._mediaElement) {
- throw new IllegalStateException('HTMLMediaElement must be attached before load()!');
- }
- if (this._transmuxer) {
- throw new IllegalStateException('FlvPlayer.load() has been called, please call unload() first!');
- }
- if (this._hasPendingLoad) {
- return;
- }
- if (this._config.deferLoadAfterSourceOpen && this._mseSourceOpened === false) {
- this._hasPendingLoad = true;
- return;
- }
- if (this._mediaElement.readyState > 0) {
- this._requestSetTime = true;
- // IE11 may throw InvalidStateError if readyState === 0
- this._mediaElement.currentTime = 0;
- }
- this._transmuxer = new Transmuxer(this._mediaDataSource, this._config);
- this._transmuxer.on(TransmuxingEvents.INIT_SEGMENT, (type, is) => {
- this._msectl.appendInitSegment(is);
- });
- this._transmuxer.on(TransmuxingEvents.MEDIA_SEGMENT, (type, ms) => {
- this._msectl.appendMediaSegment(ms);
- // lazyLoad check
- if (this._config.lazyLoad && !this._config.isLive) {
- let currentTime = this._mediaElement.currentTime;
- if (ms.info.endDts >= (currentTime + this._config.lazyLoadMaxDuration) * 1000) {
- if (this._progressChecker == null) {
- Log.v(this.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task');
- this._suspendTransmuxer();
- }
- }
- }
- });
- this._transmuxer.on(TransmuxingEvents.LOADING_COMPLETE, () => {
- this._msectl.endOfStream();
- this._emitter.emit(PlayerEvents.LOADING_COMPLETE);
- });
- this._transmuxer.on(TransmuxingEvents.RECOVERED_EARLY_EOF, () => {
- this._emitter.emit(PlayerEvents.RECOVERED_EARLY_EOF);
- });
- this._transmuxer.on(TransmuxingEvents.IO_ERROR, (detail, info) => {
- this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.NETWORK_ERROR, detail, info);
- });
- this._transmuxer.on(TransmuxingEvents.DEMUX_ERROR, (detail, info) => {
- this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.MEDIA_ERROR, detail, {code: -1, msg: info});
- });
- this._transmuxer.on(TransmuxingEvents.MEDIA_INFO, (mediaInfo) => {
- this._mediaInfo = mediaInfo;
- this._emitter.emit(PlayerEvents.MEDIA_INFO, Object.assign({}, mediaInfo));
- });
- this._transmuxer.on(TransmuxingEvents.METADATA_ARRIVED, (metadata) => {
- this._emitter.emit(PlayerEvents.METADATA_ARRIVED, metadata);
- });
- this._transmuxer.on(TransmuxingEvents.SCRIPTDATA_ARRIVED, (data) => {
- this._emitter.emit(PlayerEvents.SCRIPTDATA_ARRIVED, data);
- });
- this._transmuxer.on(TransmuxingEvents.STATISTICS_INFO, (statInfo) => {
- this._statisticsInfo = this._fillStatisticsInfo(statInfo);
- this._emitter.emit(PlayerEvents.STATISTICS_INFO, Object.assign({}, this._statisticsInfo));
- });
- this._transmuxer.on(TransmuxingEvents.RECOMMEND_SEEKPOINT, (milliseconds) => {
- if (this._mediaElement && !this._config.accurateSeek) {
- this._requestSetTime = true;
- this._mediaElement.currentTime = milliseconds / 1000;
- }
- });
- this._transmuxer.open();
- }
- unload() {
- if (this._mediaElement) {
- this._mediaElement.pause();
- }
- if (this._msectl) {
- this._msectl.seek(0);
- }
- if (this._transmuxer) {
- this._transmuxer.close();
- this._transmuxer.destroy();
- this._transmuxer = null;
- }
- }
- play() {
- return this._mediaElement.play();
- }
- pause() {
- this._mediaElement.pause();
- }
- get type() {
- return this._type;
- }
- get buffered() {
- return this._mediaElement.buffered;
- }
- get duration() {
- return this._mediaElement.duration;
- }
- get volume() {
- return this._mediaElement.volume;
- }
- set volume(value) {
- this._mediaElement.volume = value;
- }
- get muted() {
- return this._mediaElement.muted;
- }
- set muted(muted) {
- this._mediaElement.muted = muted;
- }
- get currentTime() {
- if (this._mediaElement) {
- return this._mediaElement.currentTime;
- }
- return 0;
- }
- set currentTime(seconds) {
- if (this._mediaElement) {
- this._internalSeek(seconds);
- } else {
- this._pendingSeekTime = seconds;
- }
- }
- get mediaInfo() {
- return Object.assign({}, this._mediaInfo);
- }
- get statisticsInfo() {
- if (this._statisticsInfo == null) {
- this._statisticsInfo = {};
- }
- this._statisticsInfo = this._fillStatisticsInfo(this._statisticsInfo);
- return Object.assign({}, this._statisticsInfo);
- }
- _fillStatisticsInfo(statInfo) {
- statInfo.playerType = this._type;
- if (!(this._mediaElement instanceof HTMLVideoElement)) {
- return statInfo;
- }
- let hasQualityInfo = true;
- let decoded = 0;
- let dropped = 0;
- if (this._mediaElement.getVideoPlaybackQuality) {
- let quality = this._mediaElement.getVideoPlaybackQuality();
- decoded = quality.totalVideoFrames;
- dropped = quality.droppedVideoFrames;
- } else if (this._mediaElement.webkitDecodedFrameCount != undefined) {
- decoded = this._mediaElement.webkitDecodedFrameCount;
- dropped = this._mediaElement.webkitDroppedFrameCount;
- } else {
- hasQualityInfo = false;
- }
- if (hasQualityInfo) {
- statInfo.decodedFrames = decoded;
- statInfo.droppedFrames = dropped;
- }
- return statInfo;
- }
- _onmseUpdateEnd() {
- if (!this._config.lazyLoad || this._config.isLive) {
- return;
- }
- let buffered = this._mediaElement.buffered;
- let currentTime = this._mediaElement.currentTime;
- let currentRangeStart = 0;
- let currentRangeEnd = 0;
- for (let i = 0; i < buffered.length; i++) {
- let start = buffered.start(i);
- let end = buffered.end(i);
- if (start <= currentTime && currentTime < end) {
- currentRangeStart = start;
- currentRangeEnd = end;
- break;
- }
- }
- if (currentRangeEnd >= currentTime + this._config.lazyLoadMaxDuration && this._progressChecker == null) {
- Log.v(this.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task');
- this._suspendTransmuxer();
- }
- }
- _onmseBufferFull() {
- Log.v(this.TAG, 'MSE SourceBuffer is full, suspend transmuxing task');
- if (this._progressChecker == null) {
- this._suspendTransmuxer();
- }
- }
- _suspendTransmuxer() {
- if (this._transmuxer) {
- this._transmuxer.pause();
- if (this._progressChecker == null) {
- this._progressChecker = window.setInterval(this._checkProgressAndResume.bind(this), 1000);
- }
- }
- }
- _checkProgressAndResume() {
- let currentTime = this._mediaElement.currentTime;
- let buffered = this._mediaElement.buffered;
- let needResume = false;
- for (let i = 0; i < buffered.length; i++) {
- let from = buffered.start(i);
- let to = buffered.end(i);
- if (currentTime >= from && currentTime < to) {
- if (currentTime >= to - this._config.lazyLoadRecoverDuration) {
- needResume = true;
- }
- break;
- }
- }
- if (needResume) {
- window.clearInterval(this._progressChecker);
- this._progressChecker = null;
- if (needResume) {
- Log.v(this.TAG, 'Continue loading from paused position');
- this._transmuxer.resume();
- }
- }
- }
- _isTimepointBuffered(seconds) {
- let buffered = this._mediaElement.buffered;
- for (let i = 0; i < buffered.length; i++) {
- let from = buffered.start(i);
- let to = buffered.end(i);
- if (seconds >= from && seconds < to) {
- return true;
- }
- }
- return false;
- }
- _internalSeek(seconds) {
- let directSeek = this._isTimepointBuffered(seconds);
- let directSeekBegin = false;
- let directSeekBeginTime = 0;
- if (seconds < 1.0 && this._mediaElement.buffered.length > 0) {
- let videoBeginTime = this._mediaElement.buffered.start(0);
- if ((videoBeginTime < 1.0 && seconds < videoBeginTime) || Browser.safari) {
- directSeekBegin = true;
- // also workaround for Safari: Seek to 0 may cause video stuck, use 0.1 to avoid
- directSeekBeginTime = Browser.safari ? 0.1 : videoBeginTime;
- }
- }
- if (directSeekBegin) { // seek to video begin, set currentTime directly if beginPTS buffered
- this._requestSetTime = true;
- this._mediaElement.currentTime = directSeekBeginTime;
- } else if (directSeek) { // buffered position
- if (!this._alwaysSeekKeyframe) {
- this._requestSetTime = true;
- this._mediaElement.currentTime = seconds;
- } else {
- let idr = this._msectl.getNearestKeyframe(Math.floor(seconds * 1000));
- this._requestSetTime = true;
- if (idr != null) {
- this._mediaElement.currentTime = idr.dts / 1000;
- } else {
- this._mediaElement.currentTime = seconds;
- }
- }
- if (this._progressChecker != null) {
- this._checkProgressAndResume();
- }
- } else {
- if (this._progressChecker != null) {
- window.clearInterval(this._progressChecker);
- this._progressChecker = null;
- }
- this._msectl.seek(seconds);
- this._transmuxer.seek(Math.floor(seconds * 1000)); // in milliseconds
- // no need to set mediaElement.currentTime if non-accurateSeek,
- // just wait for the recommend_seekpoint callback
- if (this._config.accurateSeek) {
- this._requestSetTime = true;
- this._mediaElement.currentTime = seconds;
- }
- }
- }
- _checkAndApplyUnbufferedSeekpoint() {
- if (this._seekpointRecord) {
- if (this._seekpointRecord.recordTime <= this._now() - 100) {
- let target = this._mediaElement.currentTime;
- this._seekpointRecord = null;
- if (!this._isTimepointBuffered(target)) {
- if (this._progressChecker != null) {
- window.clearTimeout(this._progressChecker);
- this._progressChecker = null;
- }
- // .currentTime is consists with .buffered timestamp
- // Chrome/Edge use DTS, while FireFox/Safari use PTS
- this._msectl.seek(target);
- this._transmuxer.seek(Math.floor(target * 1000));
- // set currentTime if accurateSeek, or wait for recommend_seekpoint callback
- if (this._config.accurateSeek) {
- this._requestSetTime = true;
- this._mediaElement.currentTime = target;
- }
- }
- } else {
- window.setTimeout(this._checkAndApplyUnbufferedSeekpoint.bind(this), 50);
- }
- }
- }
- _checkAndResumeStuckPlayback(stalled) {
- let media = this._mediaElement;
- if (stalled || !this._receivedCanPlay || media.readyState < 2) { // HAVE_CURRENT_DATA
- let buffered = media.buffered;
- if (buffered.length > 0 && media.currentTime < buffered.start(0)) {
- Log.w(this.TAG, `Playback seems stuck at ${media.currentTime}, seek to ${buffered.start(0)}`);
- this._requestSetTime = true;
- this._mediaElement.currentTime = buffered.start(0);
- this._mediaElement.removeEventListener('progress', this.e.onvProgress);
- }
- } else {
- // Playback didn't stuck, remove progress event listener
- this._mediaElement.removeEventListener('progress', this.e.onvProgress);
- }
- }
- _onvLoadedMetadata(e) {
- if (this._pendingSeekTime != null) {
- this._mediaElement.currentTime = this._pendingSeekTime;
- this._pendingSeekTime = null;
- }
- }
- _onvSeeking(e) { // handle seeking request from browser's progress bar
- let target = this._mediaElement.currentTime;
- let buffered = this._mediaElement.buffered;
- if (this._requestSetTime) {
- this._requestSetTime = false;
- return;
- }
- if (target < 1.0 && buffered.length > 0) {
- // seek to video begin, set currentTime directly if beginPTS buffered
- let videoBeginTime = buffered.start(0);
- if ((videoBeginTime < 1.0 && target < videoBeginTime) || Browser.safari) {
- this._requestSetTime = true;
- // also workaround for Safari: Seek to 0 may cause video stuck, use 0.1 to avoid
- this._mediaElement.currentTime = Browser.safari ? 0.1 : videoBeginTime;
- return;
- }
- }
- if (this._isTimepointBuffered(target)) {
- if (this._alwaysSeekKeyframe) {
- let idr = this._msectl.getNearestKeyframe(Math.floor(target * 1000));
- if (idr != null) {
- this._requestSetTime = true;
- this._mediaElement.currentTime = idr.dts / 1000;
- }
- }
- if (this._progressChecker != null) {
- this._checkProgressAndResume();
- }
- return;
- }
- this._seekpointRecord = {
- seekPoint: target,
- recordTime: this._now()
- };
- window.setTimeout(this._checkAndApplyUnbufferedSeekpoint.bind(this), 50);
- }
- _onvCanPlay(e) {
- this._receivedCanPlay = true;
- this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay);
- }
- _onvStalled(e) {
- this._checkAndResumeStuckPlayback(true);
- }
- _onvProgress(e) {
- this._checkAndResumeStuckPlayback();
- }
- }
- export default FlvPlayer;
|