bitmovin-player-ui
Version:
Bitmovin Player UI Framework
263 lines (222 loc) • 8.65 kB
text/typescript
import { Container, ContainerConfig } from '../Container';
import { SmallCenteredPlaybackToggleButton } from '../buttons/SmallCenteredPlaybackToggleButton';
import { PlayerAPI } from 'bitmovin-player';
import { UIInstanceManager } from '../../UIManager';
import { EventDispatcher, NoArgs, Event as EDEvent } from '../../EventDispatcher';
import { Timeout } from '../../utils/Timeout';
import { HTMLElementWithComponent } from '../../DOM';
import { Label, LabelConfig } from '../labels/Label';
import { i18n } from '../../localization/i18n';
export interface TouchControlOverlayConfig extends ContainerConfig {
/**
* Specify whether the player should be set to enter fullscreen by clicking on the playback toggle button
* when initiating the initial playback.
* Default: false.
*/
enterFullscreenOnInitialPlayback?: boolean;
/**
* Specifies whether the first touch event received by the {@link UIContainer} should be prevented or not.
*
* Default: true
*/
acceptsTouchWithUiHidden?: boolean;
/**
* Specifies how many seconds are seeked incase user seeks through double-tapping
* Default: 10sec
*/
seekTime?: number;
/**
* The second tap of a double tap has to be in a specific range of the first tap
* This specifies how many pixels off the second tap is allowed to be from the first tap
* in order to trigger the seek events
*
* Default: 15px
*/
seekDoubleTapMargin?: number;
/**
* Time in milliseconds within which two consecutive taps are considered a double tap.
* Default: 200ms
*/
seekDoubleTapTimeout?: number;
}
interface ClickPosition {
x: number;
y: number;
}
/**
* Overlays the player and detects touch input
*/
export class TouchControlOverlay extends Container<TouchControlOverlayConfig> {
private readonly SEEK_FORWARD_CLASS = 'seek-forward';
private readonly SEEK_BACKWARD_CLASS = 'seek-backward';
private touchControlEvents = {
onSingleClick: new EventDispatcher<TouchControlOverlay, NoArgs>(),
onDoubleClick: new EventDispatcher<TouchControlOverlay, NoArgs>(),
onSeekBackward: new EventDispatcher<TouchControlOverlay, NoArgs>(),
onSeekForward: new EventDispatcher<TouchControlOverlay, NoArgs>(),
};
private playbackToggleButton: SmallCenteredPlaybackToggleButton;
private seekForwardLabel: Label<LabelConfig>;
private seekBackwardLabel: Label<LabelConfig>;
// true if the last tap on the overlay was less than 500msec ago
private couldBeDoubleTapping: Boolean;
private doubleTapTimeout: Timeout;
private latestTapPosition: ClickPosition;
constructor(config: TouchControlOverlayConfig = {}) {
super(config);
this.playbackToggleButton = new SmallCenteredPlaybackToggleButton({
enterFullscreenOnInitialPlayback: Boolean(config.enterFullscreenOnInitialPlayback),
});
this.seekForwardLabel = new Label({
text: '',
for: this.getConfig().id,
cssClass: 'seek-forward-label',
hidden: true,
});
this.seekBackwardLabel = new Label({
text: '',
for: this.getConfig().id,
cssClass: 'seek-backward-label',
hidden: true,
});
this.config = this.mergeConfig(
config,
{
cssClass: 'ui-touch-control-overlay',
acceptsTouchWithUiHidden: true,
seekTime: 10,
seekDoubleTapMargin: 15,
seekDoubleTapTimeout: 200,
components: [this.seekBackwardLabel, this.playbackToggleButton, this.seekForwardLabel],
},
this.config,
);
}
configure(player: PlayerAPI, uimanager: UIInstanceManager): void {
super.configure(player, uimanager);
let playerSeekTime = 0;
let startSeekTime = 0;
this.doubleTapTimeout = new Timeout(this.config.seekDoubleTapTimeout, () => {
this.couldBeDoubleTapping = false;
startSeekTime = 0;
setTimeout(() => this.hideSeekAnimationElements(), 150);
});
let isBufferingOverlayVisible = false;
let areControlsVisible = false;
const showPlaybackToggleButton = () => {
this.playbackToggleButton.show();
};
const hidePlaybackToggleButton = () => {
this.playbackToggleButton.hide();
};
uimanager.onBufferingShow.subscribe(() => {
isBufferingOverlayVisible = true;
hidePlaybackToggleButton();
});
uimanager.onBufferingHide.subscribe(() => {
isBufferingOverlayVisible = false;
if (areControlsVisible) {
showPlaybackToggleButton();
}
});
uimanager.onControlsHide.subscribe(() => {
areControlsVisible = false;
hidePlaybackToggleButton();
});
uimanager.onControlsShow.subscribe(() => {
areControlsVisible = true;
if (!isBufferingOverlayVisible) {
showPlaybackToggleButton();
}
});
this.touchControlEvents.onSeekBackward.subscribe(() => {
playerSeekTime -= this.config.seekTime;
player.seek(playerSeekTime);
this.seekBackwardLabel.setText(
Math.abs(Math.round(playerSeekTime - startSeekTime)) +
' ' +
i18n.performLocalization(i18n.getLocalizer('settings.time.seconds')),
);
this.seekBackwardLabel.show();
this.getDomElement().addClass(this.prefixCss(this.SEEK_BACKWARD_CLASS));
this.seekForwardLabel.hide();
this.getDomElement().removeClass(this.prefixCss(this.SEEK_FORWARD_CLASS));
});
this.touchControlEvents.onSeekForward.subscribe(() => {
playerSeekTime += this.config.seekTime;
player.seek(playerSeekTime);
this.seekForwardLabel.setText(
Math.abs(Math.round(playerSeekTime - startSeekTime)) +
' ' +
i18n.performLocalization(i18n.getLocalizer('settings.time.seconds')),
);
this.seekForwardLabel.show();
this.getDomElement().addClass(this.prefixCss(this.SEEK_FORWARD_CLASS));
this.seekBackwardLabel.hide();
this.getDomElement().removeClass(this.prefixCss(this.SEEK_BACKWARD_CLASS));
});
this.touchControlEvents.onSingleClick.subscribe((_, e) => {
uimanager.getUI().toggleUiShown();
playerSeekTime = player.getCurrentTime();
startSeekTime = playerSeekTime;
const eventTarget = (e as Event).target as HTMLElementWithComponent;
const rect = eventTarget.getBoundingClientRect();
const eventTapX = (<MouseEvent>e).clientX - rect.left;
const eventTapY = (<MouseEvent>e).clientY - rect.top;
this.latestTapPosition = { x: eventTapX, y: eventTapY };
});
this.touchControlEvents.onDoubleClick.subscribe((_, e) => {
uimanager.getUI().hideUi();
const event = e as Event;
const eventTarget = event.target as HTMLElementWithComponent;
if (!eventTarget || !(eventTarget.component instanceof TouchControlOverlay)) {
return;
}
const width = eventTarget.clientWidth;
const tapMargin = width * 0.4;
const rect = eventTarget.getBoundingClientRect();
const eventTapX = (<MouseEvent>e).clientX - rect.left;
const eventTapY = (<MouseEvent>e).clientY - rect.top;
const doubleTapMargin = this.config.seekDoubleTapMargin;
if (
Math.abs(this.latestTapPosition.x - eventTapX) <= doubleTapMargin &&
Math.abs(this.latestTapPosition.y - eventTapY) <= doubleTapMargin
)
if (eventTapX < tapMargin) {
this.touchControlEvents.onSeekBackward.dispatch(this);
} else if (eventTapX > width - tapMargin) {
this.touchControlEvents.onSeekForward.dispatch(this);
}
this.latestTapPosition = { x: eventTapX, y: eventTapY };
});
this.getDomElement().on('click', e => {
if ((e.target as HTMLElementWithComponent).component instanceof TouchControlOverlay) {
clickEventDispatcher(e);
}
});
const clickEventDispatcher = (e: Event): void => {
if (this.couldBeDoubleTapping) {
this.onDoubleClickEvent(e);
} else {
this.onSingleClickEvent(e);
}
this.couldBeDoubleTapping = true;
this.doubleTapTimeout.start();
};
}
private hideSeekAnimationElements(): void {
this.getDomElement().removeClass(this.prefixCss(this.SEEK_FORWARD_CLASS));
this.getDomElement().removeClass(this.prefixCss(this.SEEK_BACKWARD_CLASS));
this.seekForwardLabel.hide();
this.seekBackwardLabel.hide();
}
protected onDoubleClickEvent(e: Event) {
this.touchControlEvents.onDoubleClick.dispatch(this, e);
}
protected onSingleClickEvent(e: Event) {
this.touchControlEvents.onSingleClick.dispatch(this, e);
}
get onClick(): EDEvent<TouchControlOverlay, NoArgs> {
return this.touchControlEvents.onSingleClick.getEvent();
}
}