UNPKG

playable

Version:

Video player based on HTML5Video

346 lines (296 loc) 10.1 kB
import HlsJs from 'hls.js/dist/hls.light.js'; import { geOverallBufferLength, getNearestBufferSegmentInfo, } from '../utils/video-data'; import { NativeEnvironmentSupport } from '../utils/environment-detection'; import { isDesktopSafari, isAndroid } from '../utils/device-detection'; import { Error as PlayableError, MediaStreamType, MediaStreamDeliveryPriority, VideoEvent, } from '../constants'; import { IPlaybackAdapter } from '../modules/playback-engine/output/native/adapters/types'; import { IEventEmitter } from '../modules/event-emitter/types'; import { IParsedPlayableSource } from '../modules/playback-engine/types'; const LIVE_SYNC_DURATION = 4; const LIVE_SYNC_DURATION_DELTA = 5; const DEFAULT_HLS_CONFIG: any = { abrEwmaDefaultEstimate: 5000 * 1000, liveSyncDuration: LIVE_SYNC_DURATION, nudgeMaxRetry: 40, // can be removed with hls v1.5.0 https://github.com/video-dev/hls.js/issues/5904#issuecomment-1762365377 }; const NETWORK_ERROR_RECOVER_TIMEOUT = 1000; const MEDIA_ERROR_RECOVER_TIMEOUT = 1000; export default class HlsAdapter implements IPlaybackAdapter { static DEFAULT_HLS_CONFIG = DEFAULT_HLS_CONFIG; static isSupported() { return NativeEnvironmentSupport.MSE && HlsJs.isSupported(); } private eventEmitter: IEventEmitter; private hls: HlsJs; private videoElement: HTMLVideoElement; private mediaStream: IParsedPlayableSource; private _mediaRecoverTimeout: number; private _networkRecoverTimeout: number; private _isDynamicContent: boolean; private _isDynamicContentEnded: boolean; private _isAttached: boolean; constructor(eventEmitter: IEventEmitter) { this.eventEmitter = eventEmitter; this.hls = null; this.videoElement = null; this.mediaStream = null; this._isDynamicContent = false; this._isDynamicContentEnded = null; this._bindCallbacks(); } private _bindCallbacks() { this._attachOnPlay = this._attachOnPlay.bind(this); this._broadcastError = this._broadcastError.bind(this); this._onEndOfStream = this._onEndOfStream.bind(this); this._onLevelUpdated = this._onLevelUpdated.bind(this); } get currentUrl() { return this.mediaStream.url; } get syncWithLiveTime(): number { if (!this.isDynamicContent) { return; } return ( this.hls.liveSyncPosition || this.videoElement.duration - LIVE_SYNC_DURATION ); } get isDynamicContent(): boolean { return this._isDynamicContent; } get isDynamicContentEnded(): boolean { return this._isDynamicContentEnded; } get isSyncWithLive(): boolean { if (!this.isDynamicContent || this.isDynamicContentEnded) { return false; } return ( this.videoElement.currentTime > this.syncWithLiveTime - LIVE_SYNC_DURATION_DELTA ); } get isSeekAvailable(): boolean { if (this.isDynamicContent && this.hls.levels) { const level = this.hls.levels[this.hls.firstLevel]; if (!level.details) { return false; } const type = level.details.type || ''; return type.trim() === 'EVENT'; } return true; } get mediaStreamDeliveryPriority() { return isDesktopSafari() || isAndroid() ? MediaStreamDeliveryPriority.FORCED : MediaStreamDeliveryPriority.ADAPTIVE_VIA_MSE; } get debugInfo() { let bitrates; let currentTime = 0; let currentBitrate = null; let nearestBufferSegInfo = null; let overallBufferLength = null; let bwEstimate = 0; const { streamController, levelController } = this.hls as any; if (levelController) { bitrates = levelController.levels.map((level: any) => level.bitrate); if (bitrates) { currentBitrate = bitrates[levelController.level]; } } if (streamController) { currentTime = streamController.lastCurrentTime; if (streamController.mediaBuffer) { overallBufferLength = geOverallBufferLength( streamController.mediaBuffer.buffered, ); nearestBufferSegInfo = getNearestBufferSegmentInfo( streamController.mediaBuffer.buffered, currentTime, ); } if (streamController.stats) { bwEstimate = streamController.stats.bwEstimate; } } return { ...this.mediaStream, bwEstimate, deliveryPriority: this.mediaStreamDeliveryPriority, bitrates, currentBitrate, overallBufferLength, nearestBufferSegInfo, }; } canPlay(mediaType: MediaStreamType) { return mediaType === MediaStreamType.HLS; } setMediaStreams(mediaStreams: IParsedPlayableSource[]) { if (mediaStreams.length === 1) { this.mediaStream = mediaStreams[0]; } else { throw new Error( `Can only handle a single HLS stream. Received ${mediaStreams.length} streams.`, ); } } private _logError(error: string, errorEvent: any) { this.eventEmitter.emitAsync(VideoEvent.ERROR, { errorType: error, streamType: MediaStreamType.HLS, streamProvider: 'hls.js', errorInstance: errorEvent, }); } private _broadcastError(_error: any, data: any) { // TODO: Investigate why this callback is called after hls is destroyed if (!this.hls) { return; } const { ErrorTypes, ErrorDetails } = HlsJs; if (data.type === ErrorTypes.NETWORK_ERROR) { switch (data.details) { case ErrorDetails.MANIFEST_LOAD_ERROR: this._logError(PlayableError.MANIFEST_LOAD, data); break; case ErrorDetails.MANIFEST_LOAD_TIMEOUT: this._logError(PlayableError.MANIFEST_LOAD, data); break; case ErrorDetails.MANIFEST_PARSING_ERROR: this._logError(PlayableError.MANIFEST_PARSE, data); break; case ErrorDetails.LEVEL_LOAD_ERROR: this._logError(PlayableError.LEVEL_LOAD, data); break; case ErrorDetails.LEVEL_LOAD_TIMEOUT: this._logError(PlayableError.LEVEL_LOAD, data); break; case ErrorDetails.AUDIO_TRACK_LOAD_ERROR: this._logError(PlayableError.CONTENT_LOAD, data); break; case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT: this._logError(PlayableError.CONTENT_LOAD, data); break; case ErrorDetails.FRAG_LOAD_ERROR: this._logError(PlayableError.CONTENT_LOAD, data); break; case ErrorDetails.FRAG_LOAD_TIMEOUT: this._logError(PlayableError.CONTENT_LOAD, data); break; default: this._logError(PlayableError.UNKNOWN, data); break; } if (data.fatal) { this._tryRecoverNetworkError(); } } else if (data.type === ErrorTypes.MEDIA_ERROR) { // NOTE: when error is BUFFER_STALLED_ERROR // video play successfully without recovering // while recover breaks video playback if (data.fatal && data.details !== ErrorDetails.BUFFER_STALLED_ERROR) { this._tryRecoverMediaError(); } switch (data.details) { case ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR: this._logError(PlayableError.MANIFEST_INCOMPATIBLE, data); break; case ErrorDetails.FRAG_PARSING_ERROR: this._logError(PlayableError.CONTENT_PARSE, data); break; default: this._logError(PlayableError.MEDIA, data); break; } } else { this._logError(PlayableError.UNKNOWN, data); } } private _tryRecoverMediaError() { if (!this._mediaRecoverTimeout) { this.hls.recoverMediaError(); this._mediaRecoverTimeout = window.setTimeout(() => { this._mediaRecoverTimeout = null; }, MEDIA_ERROR_RECOVER_TIMEOUT); } } private _tryRecoverNetworkError() { if (!this._networkRecoverTimeout) { this.hls.startLoad(); this._networkRecoverTimeout = window.setTimeout(() => { this._networkRecoverTimeout = null; }, NETWORK_ERROR_RECOVER_TIMEOUT); } } private _attachOnPlay() { if (!this.videoElement) { return; } this.hls.startLoad(); this.videoElement.removeEventListener('play', this._attachOnPlay); } private _onLevelUpdated(_eventName: string, { details }: any) { this._isDynamicContent = details.live; this._isDynamicContentEnded = details.live ? false : null; this.hls.off(HlsJs.Events.LEVEL_UPDATED, this._onLevelUpdated); } private _onEndOfStream() { if (this._isDynamicContent) { this._isDynamicContentEnded = true; this.eventEmitter.emitAsync(VideoEvent.DYNAMIC_CONTENT_ENDED); } } attach(videoElement: HTMLVideoElement) { if (!this.mediaStream) { return; } const config: any = { ...HlsAdapter.DEFAULT_HLS_CONFIG, }; this.videoElement = videoElement; if (this.videoElement.preload === 'none') { config.autoStartLoad = false; this.videoElement.addEventListener('play', this._attachOnPlay); } this.hls = new HlsJs(config); this.hls.on(HlsJs.Events.ERROR, this._broadcastError); this.hls.on(HlsJs.Events.LEVEL_UPDATED, this._onLevelUpdated); this.hls.on(HlsJs.Events.BUFFER_EOS, this._onEndOfStream); this.hls.loadSource(this.mediaStream.url); this.hls.attachMedia(this.videoElement); this._isAttached = true; } detach() { if (!this._isAttached) { return; } if (this._networkRecoverTimeout) { window.clearTimeout(this._networkRecoverTimeout); this._networkRecoverTimeout = null; } if (this._mediaRecoverTimeout) { window.clearTimeout(this._mediaRecoverTimeout); this._mediaRecoverTimeout = null; } this.hls.off(HlsJs.Events.ERROR, this._broadcastError); this.hls.off(HlsJs.Events.BUFFER_EOS, this._onEndOfStream); this.hls.off(HlsJs.Events.LEVEL_UPDATED, this._onLevelUpdated); this.hls.destroy(); this.hls = null; this.videoElement.removeEventListener('play', this._attachOnPlay); this.videoElement.removeAttribute('src'); this.videoElement = null; } }