playable
Version:
Video player based on HTML5Video
563 lines (470 loc) • 15.1 kB
text/typescript
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();
*/
showPreviewOnProgressDrag() {
this._showFullScreenPreview = true;
}
/**
* Player will seek on video when user drag the progress control
* @example
* player.seekOnProgressDrag();
*/
seekOnProgressDrag() {
this._showFullScreenPreview = false;
}
/**
* Add time indicator to progress bar
*/
addTimeIndicator(time: number) {
this.addTimeIndicators([time]);
}
/**
* Add time indicators to progress bar
*/
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
*/
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;