UNPKG

playable

Version:

Video player based on HTML5Video

563 lines (470 loc) 15.1 kB
import View from './progress.view'; import { getTimePercent, getOverallBufferedPercent, getOverallPlayedPercent, } from '../../../../utils/video-data'; import { VideoEvent, UIEvent, EngineState, LiveState, } from '../../../../constants'; import { AMOUNT_TO_SKIP_SECONDS } from '../../../keyboard-control/keyboard-control'; import KeyboardInterceptor, { KEYCODES, } from '../../../../utils/keyboard-interceptor'; import formatTime from '../../core/utils/formatTime'; import playerAPI from '../../../../core/player-api-decorator'; import { IEventEmitter } from '../../../event-emitter/types'; import { ITooltipService } from '../../core/tooltip/types'; import { IProgressControlAPI, IProgressControl, IProgressViewConfig, } from './types'; import { ITextMap } from '../../../text-map/types'; import { IPlaybackEngine } from '../../../playback-engine/types'; import { IPreviewThumbnail } from '../../preview-thumbnail/types'; import { IPreviewFullSize } from '../../preview-full-size/types'; import { IThemeService } from '../../core/theme'; export const UPDATE_PROGRESS_INTERVAL_DELAY = 1000 / 60; class ProgressControl implements IProgressControl { static moduleName = 'progressControl'; static View = View; static dependencies = [ 'engine', 'liveStateEngine', 'eventEmitter', 'textMap', 'tooltipService', 'theme', 'previewThumbnail', 'previewFullSize', ]; private _engine: IPlaybackEngine; private _liveStateEngine: any; private _eventEmitter: IEventEmitter; private _textMap: ITextMap; private _tooltipService: ITooltipService; private _theme: IThemeService; private _previewThumbnail: IPreviewThumbnail; private _previewFullSize: IPreviewFullSize; private _isUserDragging: boolean; private _shouldPlayAfterDragEnd: boolean; private _desiredSeekPosition: number; private _interceptor: KeyboardInterceptor; private _updateControlInterval: number; private _timeIndicatorsToAdd: number[]; private _shouldHidePreviewOnUpdate: boolean; private _showFullScreenPreview: boolean; private _unbindEvents: () => void; view: View; isHidden: boolean; constructor({ engine, liveStateEngine, eventEmitter, textMap, tooltipService, theme, previewThumbnail, previewFullSize, }: { eventEmitter: IEventEmitter; engine: IPlaybackEngine; liveStateEngine: any; textMap: ITextMap; tooltipService: ITooltipService; theme: IThemeService; previewThumbnail: IPreviewThumbnail; previewFullSize: IPreviewFullSize; }) { this._engine = engine; this._liveStateEngine = liveStateEngine; this._eventEmitter = eventEmitter; this._textMap = textMap; this._tooltipService = tooltipService; this._previewThumbnail = previewThumbnail; this._previewFullSize = previewFullSize; this._isUserDragging = false; this._desiredSeekPosition = 0; this._theme = theme; this._timeIndicatorsToAdd = []; this._showFullScreenPreview = false; this._bindCallbacks(); this._initUI(); this._bindEvents(); this.view.setPlayed(0); this.view.setBuffered(0); this._initInterceptor(); } getElement() { return this.view.getElement(); } private _bindEvents() { this._unbindEvents = this._eventEmitter.bindEvents( [ [VideoEvent.STATE_CHANGED, this._processStateChange], [VideoEvent.LIVE_STATE_CHANGED, this._processLiveStateChange], [VideoEvent.CHUNK_LOADED, this._updateBufferIndicator], [VideoEvent.DURATION_UPDATED, this._updateAllIndicators], [UIEvent.RESIZE, this.view.updateOnResize, this.view], ], this, ); } private _initUI() { const config: IProgressViewConfig = { callbacks: { onSyncWithLiveClick: this._syncWithLive, onSyncWithLiveMouseEnter: this._onSyncWithLiveMouseEnter, onSyncWithLiveMouseLeave: this._onSyncWithLiveMouseLeave, onChangePlayedPercent: this._onChangePlayedPercent, onSeekToByMouseStart: this._showTooltipAndPreview, onSeekToByMouseEnd: this._hideTooltip, onDragStart: this._startProcessingUserDrag, onDragEnd: this._stopProcessingUserDrag, }, theme: this._theme, textMap: this._textMap, tooltipService: this._tooltipService, }; this.view = new ProgressControl.View(config); } private _initInterceptor() { this._interceptor = new KeyboardInterceptor(this.view.getElement(), { [KEYCODES.UP_ARROW]: e => { e.stopPropagation(); e.preventDefault(); this._eventEmitter.emitAsync(UIEvent.KEYBOARD_KEYDOWN_INTERCEPTED); this._eventEmitter.emitAsync(UIEvent.GO_FORWARD_WITH_KEYBOARD); this._engine.seekForward(AMOUNT_TO_SKIP_SECONDS); }, [KEYCODES.DOWN_ARROW]: e => { e.stopPropagation(); e.preventDefault(); this._eventEmitter.emitAsync(UIEvent.KEYBOARD_KEYDOWN_INTERCEPTED); this._eventEmitter.emitAsync(UIEvent.GO_BACKWARD_WITH_KEYBOARD); this._engine.seekBackward(AMOUNT_TO_SKIP_SECONDS); }, [KEYCODES.RIGHT_ARROW]: e => { e.stopPropagation(); e.preventDefault(); this._eventEmitter.emitAsync(UIEvent.KEYBOARD_KEYDOWN_INTERCEPTED); this._eventEmitter.emitAsync(UIEvent.GO_FORWARD_WITH_KEYBOARD); this._engine.seekForward(AMOUNT_TO_SKIP_SECONDS); }, [KEYCODES.LEFT_ARROW]: e => { e.stopPropagation(); e.preventDefault(); this._eventEmitter.emitAsync(UIEvent.KEYBOARD_KEYDOWN_INTERCEPTED); this._eventEmitter.emitAsync(UIEvent.GO_BACKWARD_WITH_KEYBOARD); this._engine.seekBackward(AMOUNT_TO_SKIP_SECONDS); }, }); } private _destroyInterceptor() { this._interceptor.destroy(); } private _bindCallbacks() { this._syncWithLive = this._syncWithLive.bind(this); this._onSyncWithLiveMouseEnter = this._onSyncWithLiveMouseEnter.bind(this); this._onSyncWithLiveMouseLeave = this._onSyncWithLiveMouseLeave.bind(this); this._updateAllIndicators = this._updateAllIndicators.bind(this); this._onChangePlayedPercent = this._onChangePlayedPercent.bind(this); this._showTooltipAndPreview = this._showTooltipAndPreview.bind(this); this._hideTooltip = this._hideTooltip.bind(this); this._startProcessingUserDrag = this._startProcessingUserDrag.bind(this); this._stopProcessingUserDrag = this._stopProcessingUserDrag.bind(this); } private _startIntervalUpdates() { if (this._updateControlInterval) { this._stopIntervalUpdates(); } this._updateAllIndicators(); this._updateControlInterval = window.setInterval( this._updateAllIndicators, UPDATE_PROGRESS_INTERVAL_DELAY, ); } private _stopIntervalUpdates() { window.clearInterval(this._updateControlInterval); this._updateControlInterval = null; } private _convertPlayedPercentToTime(percent: number): number { const duration = this._engine.getDuration(); return (duration * percent) / 100; } private _onChangePlayedPercent(percent: number) { const newTime = this._convertPlayedPercentToTime(percent); if (this._showFullScreenPreview) { this._desiredSeekPosition = newTime; this._eventEmitter.emitAsync( UIEvent.PROGRESS_USER_PREVIEWING_FRAME, newTime, ); } else { this._changeCurrentTimeOfVideo(newTime); } if (this._isUserDragging) { this._showTooltipAndPreview(percent); } } private _showTooltipAndPreview(percent: number) { const duration = this._engine.getDuration(); const seekToTime = this._convertPlayedPercentToTime(percent); const timeToShow = this._engine.isDynamicContent ? seekToTime - duration : seekToTime; this._previewThumbnail.setTime(formatTime(timeToShow)); this._previewThumbnail.showAt(seekToTime); this.view.showProgressTimeTooltip( this._previewThumbnail.getElement(), percent, ); if (this._isUserDragging && this._showFullScreenPreview) { this._previewFullSize.showAt(seekToTime); } } private _hideTooltip() { if (!this._isUserDragging) { this.view.hideProgressTimeTooltip(); } } private _startProcessingUserDrag() { if (!this._isUserDragging) { this._isUserDragging = true; this._pauseVideoOnDragStart(); this._eventEmitter.emitAsync(UIEvent.PROGRESS_DRAG_STARTED); this._eventEmitter.emitAsync(UIEvent.CONTROL_DRAG_START); } } private _stopProcessingUserDrag() { if (this._isUserDragging) { this._isUserDragging = false; if (this._showFullScreenPreview) { this._shouldHidePreviewOnUpdate = true; } if (this._showFullScreenPreview) { this._changeCurrentTimeOfVideo(this._desiredSeekPosition); } this._playVideoOnDragEnd(); this.view.hideProgressTimeTooltip(); this._eventEmitter.emitAsync(UIEvent.PROGRESS_DRAG_ENDED); this._eventEmitter.emitAsync(UIEvent.CONTROL_DRAG_END); } } private _hidePreview() { this._shouldHidePreviewOnUpdate = false; this._previewFullSize.hide(); } private _processStateChange({ nextState }: { nextState: EngineState }) { switch (nextState) { case EngineState.SRC_SET: this._reset(); break; case EngineState.METADATA_LOADED: this._initTimeIndicators(); if (this._engine.isSeekAvailable) { this.show(); } else { this.hide(); } break; case EngineState.PLAYING: if (this._shouldHidePreviewOnUpdate) { this._hidePreview(); } if (this._liveStateEngine.state === LiveState.SYNC) { this.view.setPlayed(100); } else { this._startIntervalUpdates(); } break; case EngineState.PAUSED: if (this._shouldHidePreviewOnUpdate) { this._hidePreview(); } this._stopIntervalUpdates(); break; case EngineState.SEEK_IN_PROGRESS: this._updateAllIndicators(); break; default: break; } } private _processLiveStateChange({ nextState }: { nextState: LiveState }) { switch (nextState) { case LiveState.NONE: this.view.setLiveSyncState(false); this.view.setUsualMode(); break; case LiveState.INITIAL: this.view.setLiveMode(); break; case LiveState.SYNC: this.view.setLiveSyncState(true); break; case LiveState.NOT_SYNC: this.view.setLiveSyncState(false); break; case LiveState.ENDED: this.view.setLiveSyncState(false); this.view.hideSyncWithLive(); // ensure progress indicators show latest info if (this._engine.getCurrentState() === EngineState.PLAYING) { this._startIntervalUpdates(); } else { this._updateAllIndicators(); } break; default: break; } } private _changeCurrentTimeOfVideo(newTime: number) { const duration = this._engine.getDuration(); if (this._engine.isDynamicContent && duration === newTime) { this._engine.syncWithLive(); } else { this._engine.seekTo(newTime); } this._eventEmitter.emitAsync(UIEvent.PROGRESS_CHANGE, newTime); } private _pauseVideoOnDragStart() { const currentState = this._engine.getCurrentState(); if ( currentState === EngineState.PLAYING || currentState === EngineState.PLAY_REQUESTED ) { this._shouldPlayAfterDragEnd = true; this._engine.pause(); } this._eventEmitter.emitAsync(UIEvent.PROGRESS_DRAG_STARTED); } private _playVideoOnDragEnd() { if (this._shouldPlayAfterDragEnd) { this._engine.play(); this._shouldPlayAfterDragEnd = false; } } private _updateBufferIndicator() { const currentTime = this._engine.getCurrentTime(); const buffered = this._engine.getBuffered(); const duration = this._engine.getDuration(); this._setBuffered( getOverallBufferedPercent(buffered, currentTime, duration), ); } private _updatePlayedIndicator() { if (this._liveStateEngine.state === LiveState.SYNC) { // TODO: mb use this.updatePlayed(100) here? return; } const currentTime = this._engine.getCurrentTime(); const duration = this._engine.getDuration(); this._setPlayed(getOverallPlayedPercent(currentTime, duration)); } private _updateAllIndicators() { this._updatePlayedIndicator(); this._updateBufferIndicator(); } private _initTimeIndicators() { this._timeIndicatorsToAdd.forEach(time => { this._addTimeIndicator(time); }); this._timeIndicatorsToAdd = []; } private _addTimeIndicator(time: number) { const durationTime = this._engine.getDuration(); if (time > durationTime) { // TODO: log error for developers return; } this.view.addTimeIndicator(getTimePercent(time, durationTime)); } private _syncWithLive() { this._engine.syncWithLive(); } private _onSyncWithLiveMouseEnter() { this._eventEmitter.emitAsync(UIEvent.PROGRESS_SYNC_BUTTON_MOUSE_ENTER); } private _onSyncWithLiveMouseLeave() { this._eventEmitter.emitAsync(UIEvent.PROGRESS_SYNC_BUTTON_MOUSE_LEAVE); } private _setPlayed(percent: number) { this.view.setPlayed(percent); } private _setBuffered(percent: number) { this.view.setBuffered(percent); } private _reset() { this._setPlayed(0); this._setBuffered(0); this.clearTimeIndicators(); } /** * Player will show full screen preview instead of actual seek on video when user drag the progress control * @example * player.showPreviewOnProgressDrag(); */ @playerAPI() showPreviewOnProgressDrag() { this._showFullScreenPreview = true; } /** * Player will seek on video when user drag the progress control * @example * player.seekOnProgressDrag(); */ @playerAPI() seekOnProgressDrag() { this._showFullScreenPreview = false; } /** * Add time indicator to progress bar */ @playerAPI() addTimeIndicator(time: number) { this.addTimeIndicators([time]); } /** * Add time indicators to progress bar */ @playerAPI() addTimeIndicators(times: number[]) { if (!this._engine.isMetadataLoaded) { // NOTE: Add indicator after metadata loaded this._timeIndicatorsToAdd.push(...times); return; } times.forEach(time => { this._addTimeIndicator(time); }); } /** * Delete all time indicators from progress bar */ @playerAPI() clearTimeIndicators() { this.view.clearTimeIndicators(); } hide() { this.isHidden = true; this.view.hide(); } show() { this.isHidden = false; this.view.show(); } destroy() { this._destroyInterceptor(); this._stopIntervalUpdates(); this._unbindEvents(); this.view.destroy(); } } export { IProgressControlAPI }; export default ProgressControl;