bitmovin-player-ui
Version:
Bitmovin Player UI Framework
461 lines (460 loc) • 21.5 kB
JavaScript
;
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.UIContainer = void 0;
var Container_1 = require("./Container");
var DOM_1 = require("../DOM");
var Timeout_1 = require("../utils/Timeout");
var PlayerUtils_1 = require("../utils/PlayerUtils");
var EventDispatcher_1 = require("../EventDispatcher");
var i18n_1 = require("../localization/i18n");
var Button_1 = require("./buttons/Button");
var TouchControlOverlay_1 = require("./overlays/TouchControlOverlay");
var SettingsPanel_1 = require("./settings/SettingsPanel");
/**
* The base container that contains all of the UI. The UIContainer is passed to the {@link UIManager} to build and
* setup the UI.
*
* @category Containers
*/
var UIContainer = /** @class */ (function (_super) {
__extends(UIContainer, _super);
function UIContainer(config) {
var _this = _super.call(this, config) || this;
_this.hideUi = function () { };
_this.showUi = function () { };
_this.toggleUiShown = function () { };
_this.config = _this.mergeConfig(config, {
cssClass: 'ui-uicontainer',
role: 'region',
ariaLabel: i18n_1.i18n.getLocalizer('player'),
hideDelay: 2000,
hideImmediatelyOnMouseLeave: true,
deferUiHideWhileSettingsOpen: true,
}, _this.config);
_this.playerStateChange = new EventDispatcher_1.EventDispatcher();
_this.hidingPrevented = function () { return false; };
return _this;
}
UIContainer.prototype.configure = function (player, uimanager) {
var config = this.getConfig();
if (config.userInteractionEventSource) {
this.userInteractionEventSource = new DOM_1.DOM(config.userInteractionEventSource);
}
else {
this.userInteractionEventSource = this.getDomElement();
}
_super.prototype.configure.call(this, player, uimanager);
this.configureUIShowHide(player, uimanager);
this.configurePlayerStates(player, uimanager);
};
UIContainer.prototype.configureUIShowHide = function (player, uimanager) {
var _this = this;
var config = this.getConfig();
var isUiShown = false;
var isSettingsPanelShown = false;
var isHideUiPending = false;
uimanager.onConfigured.subscribe(function () {
if (isUiShown) {
uimanager.onControlsShow.dispatch(_this);
}
else {
uimanager.onControlsHide.dispatch(_this);
}
});
if (config.hideDelay === -1) {
isUiShown = true;
return;
}
var isSeeking = false;
var isFirstTouch = true;
var playerState;
if (config.deferUiHideWhileSettingsOpen) {
uimanager.onComponentShow.subscribe(function (component) {
if (component instanceof SettingsPanel_1.SettingsPanel) {
isSettingsPanelShown = true;
}
});
uimanager.onComponentHide.subscribe(function (component) {
if (component instanceof SettingsPanel_1.SettingsPanel) {
isSettingsPanelShown = false;
if (isHideUiPending) {
_this.hideUi(true);
isHideUiPending = false;
}
}
});
}
this.hidingPrevented = function () {
return config.hidePlayerStateExceptions && config.hidePlayerStateExceptions.indexOf(playerState) > -1;
};
this.showUi = function () {
isHideUiPending = false;
if (!isUiShown) {
// Let subscribers know that they should reveal themselves
uimanager.onControlsShow.dispatch(_this);
isUiShown = true;
}
// Don't trigger timeout while seeking (it will be triggered once the seek is finished) or casting
if (!isSeeking && !player.isCasting() && !_this.hidingPrevented()) {
_this.uiHideTimeout.start();
}
};
this.hideUi = function (force) {
if (force === void 0) { force = false; }
// Hide the UI only if it is shown, and if not casting
if (isUiShown && !player.isCasting()) {
if (force) {
uimanager.onControlsHide.dispatch(_this);
isUiShown = false;
return;
}
if (config.deferUiHideWhileSettingsOpen && isSettingsPanelShown) {
isHideUiPending = true;
return;
}
// Issue a preview event to check if we are good to hide the controls
var previewHideEventArgs = {};
uimanager.onPreviewControlsHide.dispatch(_this, previewHideEventArgs);
if (!previewHideEventArgs.cancel) {
// If the preview wasn't canceled, let subscribers know that they should now hide themselves
uimanager.onControlsHide.dispatch(_this);
isUiShown = false;
}
else {
// If the hide preview was canceled, continue to show UI
_this.showUi();
}
}
};
this.toggleUiShown = function () {
isUiShown ? _this.hideUi() : _this.showUi();
};
// Timeout to defer UI hiding by the configured delay time
this.uiHideTimeout = new Timeout_1.Timeout(config.hideDelay, this.hideUi);
var checkActionAllowed = function (e) {
/**
* The super-modern-UI has its own component, with its own listeners,
* to detect touches on empty space {@link TouchControlOverlay}.
* Because the {@link UIContainer} is the root container, it also detects these touches.
* In order to let the {@link TouchControlOverlay} do its work correctly,
* we check if the touched target is an instance of it.
*/
return !(e.target.component instanceof TouchControlOverlay_1.TouchControlOverlay);
};
this.userInteractionEvents = [
{
// On touch displays, the first touch reveals the UI
name: 'touchend',
handler: function (e) {
if (!checkActionAllowed(e)) {
return;
}
var shouldPreventDefault = function (e) {
var findButtonComponent = function (element) {
if (!element ||
element === _this.userInteractionEventSource.get(0) ||
element.component instanceof UIContainer) {
return null;
}
if ((element.component && element.component instanceof Button_1.Button) ||
element.component instanceof TouchControlOverlay_1.TouchControlOverlay) {
return element.component;
}
else {
return findButtonComponent(element.parentElement);
}
};
var buttonComponent = findButtonComponent(e.target);
return !(buttonComponent && buttonComponent.getConfig().acceptsTouchWithUiHidden);
};
if (!isUiShown) {
// Only if the UI is hidden, we prevent other actions (except for the first touch) and reveal the UI
// instead. The first touch is not prevented to let other listeners receive the event and trigger an
// initial action, e.g. the huge playback button can directly start playback instead of requiring a double
// tap which 1. reveals the UI and 2. starts playback.
if (isFirstTouch && !player.isPlaying()) {
isFirstTouch = false;
}
else {
// On touch input devices, the first touch is expected to display the UI controls and not be propagated to
// other components.
// When buttons are always visible this causes UX problems, as the first touch is not recognized.
// This is the case for the {@link AdSkipButton} and {@link AdClickOverlay}.
// To prevent UX issues where the buttons need to be touched twice, we do not prevent the first touch event.
if (shouldPreventDefault(e)) {
e.preventDefault();
}
}
_this.showUi();
}
},
},
{
// When the mouse enters, we show the UI
name: 'mouseenter',
handler: function (e) {
if (checkActionAllowed(e)) {
_this.showUi();
}
},
},
{
// When the mouse moves within, we show the UI
name: 'mousemove',
handler: function (e) {
if (checkActionAllowed(e)) {
_this.showUi();
}
},
},
{
name: 'focusin',
handler: function (e) {
if (checkActionAllowed(e)) {
_this.showUi();
}
},
},
{
name: 'keydown',
handler: function (e) {
if (checkActionAllowed(e)) {
_this.showUi();
}
},
},
{
// When the mouse leaves, we can prepare to hide the UI, except a seek is going on
name: 'mouseleave',
handler: function () {
// When a seek is going on, the seek scrub pointer may exit the UI area while still seeking, and we do not
// hide the UI in such cases
if (!isSeeking && !_this.hidingPrevented()) {
if (_this.config.hideImmediatelyOnMouseLeave) {
_this.hideUi(true);
}
else {
_this.uiHideTimeout.start();
}
}
},
},
{
// When scrolling, we show the UI
name: 'wheel',
handler: function (e) {
if (checkActionAllowed(e)) {
_this.showUi();
}
},
},
];
this.userInteractionEvents.forEach(function (event) { return _this.userInteractionEventSource.on(event.name, event.handler); });
// Add click listener running on capture phase to intercept clicks before stopPropagation() in buttons
this.userInteractionEventSource.on('click', function (e) {
if (checkActionAllowed(e)) {
_this.showUi();
}
}, { capture: true });
uimanager.onSeek.subscribe(function () {
_this.uiHideTimeout.clear(); // Don't hide UI while a seek is in progress
isSeeking = true;
});
uimanager.onSeeked.subscribe(function () {
isSeeking = false;
if (!_this.hidingPrevented()) {
_this.uiHideTimeout.start(); // Re-enable UI hide timeout after a seek
}
});
uimanager.onComponentViewModeChanged.subscribe(function (_, _a) {
var mode = _a.mode;
return _this.trackComponentViewMode(mode);
});
player.on(player.exports.PlayerEvent.CastStarted, function () {
_this.showUi(); // Show UI when a Cast session has started (UI will then stay permanently on during the session)
});
this.playerStateChange.subscribe(function (_, state) {
playerState = state;
if (_this.hidingPrevented()) {
// Entering a player state that prevents hiding and forces the controls to be shown
_this.uiHideTimeout.clear();
_this.showUi();
}
else {
// Entering a player state that allows hiding
_this.uiHideTimeout.start();
}
});
};
UIContainer.prototype.configurePlayerStates = function (player, uimanager) {
var _this = this;
var container = this.getDomElement();
// Convert player states into CSS class names
var stateClassNames = [];
for (var state in PlayerUtils_1.PlayerUtils.PlayerState) {
if (isNaN(Number(state))) {
var enumName = PlayerUtils_1.PlayerUtils.PlayerState[PlayerUtils_1.PlayerUtils.PlayerState[state]];
stateClassNames[PlayerUtils_1.PlayerUtils.PlayerState[state]] = this.prefixCss(UIContainer.STATE_PREFIX + enumName.toLowerCase());
}
}
var removeStates = function () {
container.removeClass(stateClassNames[PlayerUtils_1.PlayerUtils.PlayerState.Idle]);
container.removeClass(stateClassNames[PlayerUtils_1.PlayerUtils.PlayerState.Prepared]);
container.removeClass(stateClassNames[PlayerUtils_1.PlayerUtils.PlayerState.Playing]);
container.removeClass(stateClassNames[PlayerUtils_1.PlayerUtils.PlayerState.Paused]);
container.removeClass(stateClassNames[PlayerUtils_1.PlayerUtils.PlayerState.Finished]);
};
var updateState = function (state) {
removeStates();
container.addClass(stateClassNames[state]);
_this.playerStateChange.dispatch(_this, state);
};
player.on(player.exports.PlayerEvent.SourceLoaded, function () {
updateState(PlayerUtils_1.PlayerUtils.PlayerState.Prepared);
});
player.on(player.exports.PlayerEvent.Play, function () {
updateState(PlayerUtils_1.PlayerUtils.PlayerState.Playing);
});
player.on(player.exports.PlayerEvent.Playing, function () {
updateState(PlayerUtils_1.PlayerUtils.PlayerState.Playing);
});
player.on(player.exports.PlayerEvent.Paused, function () {
updateState(PlayerUtils_1.PlayerUtils.PlayerState.Paused);
});
player.on(player.exports.PlayerEvent.PlaybackFinished, function () {
updateState(PlayerUtils_1.PlayerUtils.PlayerState.Finished);
});
player.on(player.exports.PlayerEvent.SourceUnloaded, function () {
updateState(PlayerUtils_1.PlayerUtils.PlayerState.Idle);
});
uimanager.getConfig().events.onUpdated.subscribe(function () {
updateState(PlayerUtils_1.PlayerUtils.getState(player));
});
// Fullscreen marker class
player.on(player.exports.PlayerEvent.ViewModeChanged, function () {
if (player.getViewMode() === player.exports.ViewMode.Fullscreen) {
container.addClass(_this.prefixCss(UIContainer.FULLSCREEN));
}
else {
container.removeClass(_this.prefixCss(UIContainer.FULLSCREEN));
}
});
// Init fullscreen state
if (player.getViewMode() === player.exports.ViewMode.Fullscreen) {
container.addClass(this.prefixCss(UIContainer.FULLSCREEN));
}
// Buffering marker class
player.on(player.exports.PlayerEvent.StallStarted, function () {
container.addClass(_this.prefixCss(UIContainer.BUFFERING));
});
player.on(player.exports.PlayerEvent.StallEnded, function () {
container.removeClass(_this.prefixCss(UIContainer.BUFFERING));
});
// Init buffering state
if (player.isStalled()) {
container.addClass(this.prefixCss(UIContainer.BUFFERING));
}
// RemoteControl marker class
player.on(player.exports.PlayerEvent.CastStarted, function () {
container.addClass(_this.prefixCss(UIContainer.REMOTE_CONTROL));
});
player.on(player.exports.PlayerEvent.CastStopped, function () {
container.removeClass(_this.prefixCss(UIContainer.REMOTE_CONTROL));
});
// Init RemoteControl state
if (player.isCasting()) {
container.addClass(this.prefixCss(UIContainer.REMOTE_CONTROL));
}
// Controls visibility marker class
uimanager.onControlsShow.subscribe(function () {
container.removeClass(_this.prefixCss(UIContainer.CONTROLS_HIDDEN));
container.addClass(_this.prefixCss(UIContainer.CONTROLS_SHOWN));
});
uimanager.onControlsHide.subscribe(function () {
container.removeClass(_this.prefixCss(UIContainer.CONTROLS_SHOWN));
container.addClass(_this.prefixCss(UIContainer.CONTROLS_HIDDEN));
});
// Layout size classes
var updateLayoutSizeClasses = function (width, height) {
container.removeClass(_this.prefixCss('layout-max-width-400'));
container.removeClass(_this.prefixCss('layout-max-width-600'));
container.removeClass(_this.prefixCss('layout-max-width-800'));
container.removeClass(_this.prefixCss('layout-max-width-1200'));
if (width <= 400) {
container.addClass(_this.prefixCss('layout-max-width-400'));
}
else if (width <= 600) {
container.addClass(_this.prefixCss('layout-max-width-600'));
}
else if (width <= 800) {
container.addClass(_this.prefixCss('layout-max-width-800'));
}
else if (width <= 1200) {
container.addClass(_this.prefixCss('layout-max-width-1200'));
}
};
player.on(player.exports.PlayerEvent.PlayerResized, function (e) {
// Convert strings (with "px" suffix) to ints
var width = Math.round(Number(e.width.substring(0, e.width.length - 2)));
var height = Math.round(Number(e.height.substring(0, e.height.length - 2)));
updateLayoutSizeClasses(width, height);
});
// Init layout state
updateLayoutSizeClasses(new DOM_1.DOM(player.getContainer()).width(), new DOM_1.DOM(player.getContainer()).height());
};
UIContainer.prototype.release = function () {
var _this = this;
// Explicitly unsubscribe user interaction event handlers because they could be attached to an external element
// that isn't owned by the UI and therefore not removed on release.
if (this.userInteractionEvents) {
this.userInteractionEvents.forEach(function (event) { return _this.userInteractionEventSource.off(event.name, event.handler); });
}
_super.prototype.release.call(this);
if (this.uiHideTimeout) {
this.uiHideTimeout.clear();
}
};
UIContainer.prototype.onPlayerStateChange = function () {
return this.playerStateChange.getEvent();
};
UIContainer.prototype.suspendHideTimeout = function () {
this.uiHideTimeout.suspend();
};
UIContainer.prototype.resumeHideTimeout = function () {
this.uiHideTimeout.resume(!this.hidingPrevented());
};
UIContainer.prototype.toDomElement = function () {
var container = _super.prototype.toDomElement.call(this);
// Detect flexbox support (not supported in IE9)
if (document && typeof document.createElement('p').style.flex !== 'undefined') {
container.addClass(this.prefixCss('flexbox'));
}
else {
container.addClass(this.prefixCss('no-flexbox'));
}
return container;
};
UIContainer.STATE_PREFIX = 'player-state-';
UIContainer.FULLSCREEN = 'fullscreen';
UIContainer.BUFFERING = 'buffering';
UIContainer.REMOTE_CONTROL = 'remote-control';
UIContainer.CONTROLS_SHOWN = 'controls-shown';
UIContainer.CONTROLS_HIDDEN = 'controls-hidden';
return UIContainer;
}(Container_1.Container));
exports.UIContainer = UIContainer;