bitmovin-player-ui
Version:
Bitmovin Player UI Framework
178 lines (153 loc) • 7.4 kB
text/typescript
import { PlaybackToggleButton, PlaybackToggleButtonConfig } from './PlaybackToggleButton';
import { DOM } from '../../DOM';
import { UIInstanceManager } from '../../UIManager';
import { PlayerAPI, PlayerEventBase, WarningEvent } from 'bitmovin-player';
import { i18n } from '../../localization/i18n';
/**
* A button that overlays the video and toggles between playback and pause.
*
* @category Buttons
*/
export class HugePlaybackToggleButton extends PlaybackToggleButton {
constructor(config: PlaybackToggleButtonConfig = {}) {
super(config);
this.config = this.mergeConfig(
config,
{
cssClass: 'ui-hugeplaybacktogglebutton',
text: i18n.getLocalizer('playPause'),
role: 'button',
},
this.config,
);
}
configure(player: PlayerAPI, uimanager: UIInstanceManager): void {
// Update button state through API events
super.configure(player, uimanager, false);
// Set enterFullscreenOnInitialPlayback if set in the uimanager config
if (typeof uimanager.getConfig().enterFullscreenOnInitialPlayback === 'boolean') {
this.config.enterFullscreenOnInitialPlayback = uimanager.getConfig().enterFullscreenOnInitialPlayback;
}
const togglePlayback = () => {
if (player.isPlaying() || this.isPlayInitiated) {
player.pause('ui');
} else {
player.play('ui');
}
};
const toggleFullscreen = () => {
if (player.getViewMode() === player.exports.ViewMode.Fullscreen) {
player.setViewMode(player.exports.ViewMode.Inline);
} else {
player.setViewMode(player.exports.ViewMode.Fullscreen);
}
};
let firstPlay = true;
let clickTime = 0;
let doubleClickTime = 0;
/*
* YouTube-style toggle button handling
*
* The goal is to prevent a short pause or playback interval between a click, that toggles playback, and a
* double click, that toggles fullscreen. In this naive approach, the first click would e.g. start playback,
* the second click would be detected as double click and toggle to fullscreen, and as second normal click stop
* playback, which results is a short playback interval with max length of the double click detection
* period (usually 500ms).
*
* To solve this issue, we defer handling of the first click for 200ms, which is almost unnoticeable to the user,
* and just toggle playback if no second click (double click) has been registered during this period. If a double
* click is registered, we just toggle the fullscreen. In the first 200ms, undesired playback changes thus cannot
* happen. If a double click is registered within 500ms, we undo the playback change and switch fullscreen mode.
* In the end, this method basically introduces a 200ms observing interval in which playback changes are prevented
* if a double click happens.
*/
this.onClick.subscribe(() => {
// Directly start playback on first click of the button.
// This is a required workaround for mobile browsers where video playback needs to be triggered directly
// by the user. A deferred playback start through the timeout below is not considered as user action and
// therefore ignored by mobile browsers.
if (firstPlay) {
// Try to start playback. Then we wait for Play and only when it arrives, we disable the firstPlay flag.
// If we disable the flag here, onClick was triggered programmatically instead of by a user interaction, and
// playback is blocked (e.g. on mobile devices due to the programmatic play() call), we loose the chance to
// ever start playback through a user interaction again with this button.
togglePlayback();
if (this.config.enterFullscreenOnInitialPlayback) {
player.setViewMode(player.exports.ViewMode.Fullscreen);
}
return;
}
const now = Date.now();
if (now - clickTime < 200) {
// We have a double click inside the 200ms interval, just toggle fullscreen mode
toggleFullscreen();
doubleClickTime = now;
return;
} else if (now - clickTime < 500) {
// We have a double click inside the 500ms interval, undo playback toggle and toggle fullscreen mode
toggleFullscreen();
togglePlayback();
doubleClickTime = now;
return;
}
clickTime = now;
setTimeout(() => {
if (Date.now() - doubleClickTime > 200) {
// No double click detected, so we toggle playback and wait what happens next
togglePlayback();
}
}, 200);
});
player.on(player.exports.PlayerEvent.Play, () => {
// Playback has really started, we can disable the flag to switch to normal toggle button handling
firstPlay = false;
});
player.on(player.exports.PlayerEvent.Warning, (event: WarningEvent) => {
if (event.code === player.exports.WarningCode.PLAYBACK_COULD_NOT_BE_STARTED) {
// if playback could not be started, reset the first play flag as we need the user interaction to start
firstPlay = true;
}
});
const suppressPlayButtonTransitionAnimation = () => {
// Disable the current animation
this.setTransitionAnimationsEnabled(false);
// Enable the transition animations for the next state change
this.onToggle.subscribeOnce(() => {
this.setTransitionAnimationsEnabled(true);
});
};
// Hide the play button animation when the UI is loaded (it should only be animated on state changes)
suppressPlayButtonTransitionAnimation();
const isAutoplayEnabled = player.getConfig().playback && Boolean(player.getConfig().playback.autoplay);
// We only know if an autoplay attempt is upcoming if the player is not yet ready. It the player is already ready,
// the attempt might be upcoming or might have already happened, but we don't have to handle that because we can
// simply rely on isPlaying and the play state events.
const isAutoplayUpcoming = !player.getSource() && isAutoplayEnabled;
// Hide the play button when the player is already playing or autoplay is upcoming
if (player.isPlaying() || isAutoplayUpcoming) {
// Hide the play button (switch to playing state)
this.on();
// Disable the animation of the playing state switch
suppressPlayButtonTransitionAnimation();
// Show the play button without an animation if a play attempt is blocked
player.on(player.exports.PlayerEvent.Warning, (event: WarningEvent) => {
if (event.code === player.exports.WarningCode.PLAYBACK_COULD_NOT_BE_STARTED) {
suppressPlayButtonTransitionAnimation();
}
});
}
}
/**
* Enables or disables the play state transition animations of the play button image. Can be used to suppress
* animations.
* @param {boolean} enabled true to enable the animations (default), false to disable them
*/
protected setTransitionAnimationsEnabled(enabled: boolean): void {
const noTransitionAnimationsClass = this.prefixCss('no-transition-animations');
if (enabled) {
this.getDomElement().removeClass(noTransitionAnimationsClass);
} else if (!this.getDomElement().hasClass(noTransitionAnimationsClass)) {
this.getDomElement().addClass(noTransitionAnimationsClass);
}
}
}