mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-24 17:35:10 +03:00
f39bade337
This is different from the giant play/pause button that sits over the video container. Change-Id: I4629b3a64a5208f23c65b383f4fb992e6ddfd334
1374 lines
40 KiB
JavaScript
1374 lines
40 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2016 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
|
|
goog.provide('shaka.ui.Controls');
|
|
goog.provide('shaka.ui.ControlsPanel');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.ui.Constants');
|
|
goog.require('shaka.ui.Enums');
|
|
goog.require('shaka.ui.Locales');
|
|
goog.require('shaka.ui.Localization');
|
|
goog.require('shaka.ui.Utils');
|
|
goog.require('shaka.util.Dom');
|
|
goog.require('shaka.util.EventManager');
|
|
goog.require('shaka.util.FakeEvent');
|
|
goog.require('shaka.util.FakeEventTarget');
|
|
goog.require('shaka.util.Timer');
|
|
|
|
|
|
/**
|
|
* A container for custom video controls.
|
|
* @param {!shaka.Player} player
|
|
* @param {!HTMLElement} videoContainer
|
|
* @param {!HTMLMediaElement} video
|
|
* @param {shaka.extern.UIConfiguration} config
|
|
* @constructor
|
|
* @struct
|
|
* @implements {shaka.util.IDestroyable}
|
|
* @extends {shaka.util.FakeEventTarget}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls = function(player, videoContainer, video, config) {
|
|
shaka.util.FakeEventTarget.call(this);
|
|
|
|
/** @private {boolean} */
|
|
this.enabled_ = true;
|
|
|
|
/** @private {boolean} */
|
|
this.overrideCssShowControls_ = false;
|
|
|
|
/** shaka.extern.UIConfiguration */
|
|
this.config_ = config;
|
|
|
|
/** @private {!shaka.cast.CastProxy} */
|
|
this.castProxy_ = new shaka.cast.CastProxy(
|
|
video, player, this.config_.castReceiverAppId);
|
|
|
|
/** @private {boolean} */
|
|
this.castAllowed_ = true;
|
|
|
|
/** @private {!HTMLMediaElement} */
|
|
this.video_ = this.castProxy_.getVideo();
|
|
|
|
/** @private {!HTMLMediaElement} */
|
|
this.localVideo_ = video;
|
|
|
|
/** @private {!shaka.Player} */
|
|
this.player_ = this.castProxy_.getPlayer();
|
|
|
|
/** @private {!HTMLElement} */
|
|
this.videoContainer_ = videoContainer;
|
|
|
|
/** @private {boolean} */
|
|
this.isSeeking_ = false;
|
|
|
|
/**
|
|
* This timer is used to introduce a delay between the user scrubbing across
|
|
* the seek bar and the seek being sent to the player.
|
|
*
|
|
* @private {shaka.util.Timer}
|
|
*/
|
|
this.seekTimer_ = new shaka.util.Timer(() => {
|
|
goog.asserts.assert(this.seekBar_ != null, 'Seekbar should not be null!');
|
|
this.video_.currentTime = parseFloat(this.seekBar_.value);
|
|
});
|
|
|
|
/**
|
|
* This timer is used to detect when the user has stopped moving the mouse
|
|
* and we should fade out the ui.
|
|
*
|
|
* @private {shaka.util.Timer}
|
|
*/
|
|
this.mouseStillTimer_ = new shaka.util.Timer(() => {
|
|
this.onMouseStill_();
|
|
});
|
|
|
|
/**
|
|
* This timer will be used to hide all settings menus. When the timer ticks
|
|
* it will force all controls to invisible.
|
|
*
|
|
* Rather than calling the callback directly, |Controls| will always call it
|
|
* through the timer to avoid conflicts.
|
|
*
|
|
* @private {shaka.util.Timer}
|
|
*/
|
|
this.hideSettingsMenusTimer_ = new shaka.util.Timer(() => {
|
|
/** type {function(!HTMLElement)} */
|
|
const hide = (control) => {
|
|
shaka.ui.Utils.setDisplay(control, /* visible= */ false);
|
|
};
|
|
|
|
for (const menu of this.settingsMenus_) {
|
|
hide(/** @type {!HTMLElement} */ (menu));
|
|
}
|
|
});
|
|
|
|
/**
|
|
* This timer is used to regularly update the time and seek range elements
|
|
* so that we are communicating the current state as accurately as possibly.
|
|
*
|
|
* Unlike the other timers, this timer does not "own" the callback because
|
|
* this timer is acting like a heartbeat.
|
|
*
|
|
* @private {shaka.util.Timer}
|
|
*/
|
|
this.timeAndSeekRangeTimer_ = new shaka.util.Timer(() => {
|
|
this.updateTimeAndSeekRange_();
|
|
});
|
|
|
|
/** @private {?number} */
|
|
this.lastTouchEventTime_ = null;
|
|
|
|
/** @private {!Array.<!shaka.extern.IUIElement>} */
|
|
this.elements_ = [];
|
|
|
|
/** @private {shaka.ui.Localization} */
|
|
this.localization_ = shaka.ui.Controls.createLocalization_();
|
|
|
|
this.createDOM_();
|
|
|
|
this.updateLocalizedStrings_();
|
|
|
|
/** @private {shaka.util.EventManager} */
|
|
this.eventManager_ = new shaka.util.EventManager();
|
|
|
|
this.addEventListeners_();
|
|
|
|
/**
|
|
* The pressed keys set is used to record which keys are currently pressed
|
|
* down, so we can know what keys are pressed at the same time.
|
|
* Used by the focusInsideOverflowMenu_() function.
|
|
* @private {!Set.<number>}
|
|
*/
|
|
this.pressedKeys_ = new Set();
|
|
|
|
// We might've missed a caststatuschanged event from the proxy between
|
|
// the controls creation and initializing. Run onCastStatusChange_()
|
|
// to ensure we have the casting state right.
|
|
this.onCastStatusChange_(null);
|
|
|
|
// Start this timer after we are finished initializing everything,
|
|
this.timeAndSeekRangeTimer_.tickEvery(/* seconds= */ 0.125);
|
|
};
|
|
|
|
goog.inherits(shaka.ui.Controls, shaka.util.FakeEventTarget);
|
|
|
|
|
|
/** @private {!Map.<string, !shaka.extern.IUIElement.Factory>} */
|
|
shaka.ui.ControlsPanel.elementNamesToFactories_ = new Map();
|
|
|
|
|
|
/**
|
|
* @override
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.destroy = async function() {
|
|
if (this.eventManager_) {
|
|
this.eventManager_.release();
|
|
this.eventManager_ = null;
|
|
}
|
|
|
|
if (this.seekTimer_) {
|
|
this.seekTimer_.stop();
|
|
this.seekTimer_ = null;
|
|
}
|
|
|
|
if (this.mouseStillTimer_) {
|
|
this.mouseStillTimer_.stop();
|
|
this.mouseStillTimer_ = null;
|
|
}
|
|
|
|
if (this.hideSettingsMenusTimer_) {
|
|
this.hideSettingsMenusTimer_.stop();
|
|
this.hideSettingsMenusTimer_ = null;
|
|
}
|
|
|
|
if (this.timeAndSeekRangeTimer_) {
|
|
this.timeAndSeekRangeTimer_.stop();
|
|
this.timeAndSeekRangeTimer_ = null;
|
|
}
|
|
|
|
this.localization_ = null;
|
|
this.pressedKeys_.clear();
|
|
|
|
await Promise.all(this.elements_.map((element) => element.destroy()));
|
|
this.elements_ = [];
|
|
};
|
|
|
|
|
|
/**
|
|
* @event shaka.Controls.CastStatusChangedEvent
|
|
* @description Fired upon receiving a 'caststatuschanged' event from
|
|
* the cast proxy.
|
|
* @property {string} type
|
|
* 'caststatuschanged'
|
|
* @property {boolean} newStatus
|
|
* The new status of the application. True for 'is casting' and
|
|
* false otherwise.
|
|
* @exportDoc
|
|
*/
|
|
|
|
|
|
/**
|
|
* @event shaka.Controls.SubMenuOpenEvent
|
|
* @description Fired when one of the overflow submenus is opened
|
|
* (e. g. language/resolution/subtitle selection).
|
|
* @property {string} type
|
|
* 'submenuopen'
|
|
* @exportDoc
|
|
*/
|
|
|
|
|
|
/**
|
|
* @event shaka.Controls.CaptionSelectionUpdatedEvent
|
|
* @description Fired when the captions/subtitles menu has finished updating.
|
|
* @property {string} type
|
|
* 'captionselectionupdated'
|
|
* @exportDoc
|
|
*/
|
|
|
|
|
|
/**
|
|
* @event shaka.Controls.ResolutionSelectionUpdatedEvent
|
|
* @description Fired when the resolution menu has finished updating.
|
|
* @property {string} type
|
|
* 'resolutionselectionupdated'
|
|
* @exportDoc
|
|
*/
|
|
|
|
|
|
/**
|
|
* @event shaka.Controls.LanguageSelectionUpdatedEvent
|
|
* @description Fired when the audio language menu has finished updating.
|
|
* @property {string} type
|
|
* 'languageselectionupdated'
|
|
* @exportDoc
|
|
*/
|
|
|
|
|
|
/**
|
|
* @event shaka.Controls.ErrorEvent
|
|
* @description Fired when something went wrong with the controls.
|
|
* @property {string} type
|
|
* 'error'
|
|
* @property {!shaka.util.Error} detail
|
|
* An object which contains details on the error. The error's 'category' and
|
|
* 'code' properties will identify the specific error that occurred. In an
|
|
* uncompiled build, you can also use the 'message' and 'stack' properties
|
|
* to debug.
|
|
* @exportDoc
|
|
*/
|
|
|
|
|
|
/**
|
|
* @event shaka.Controls.TimeAndSeekRangeUpdatedEvent
|
|
* @description Fired when the time and seek range elements have finished
|
|
* updating.
|
|
* @property {string} type
|
|
* 'timeandseekrangeupdated'
|
|
* @exportDoc
|
|
*/
|
|
|
|
|
|
/**
|
|
* @event shaka.Controls.UIUpdatedEvent
|
|
* @description Fired after a call to ui.configure() once the UI has finished
|
|
* updating.
|
|
* @property {string} type
|
|
* 'uiupdated'
|
|
* @exportDoc
|
|
*/
|
|
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {!shaka.extern.IUIElement.Factory} factory
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.registerElement = function(name, factory) {
|
|
shaka.ui.ControlsPanel.elementNamesToFactories_.set(name, factory);
|
|
};
|
|
|
|
|
|
/**
|
|
* This allows the application to inhibit casting.
|
|
*
|
|
* @param {boolean} allow
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.allowCast = function(allow) {
|
|
this.castAllowed_ = allow;
|
|
this.onCastStatusChange_(null);
|
|
};
|
|
|
|
|
|
/**
|
|
* Used by the application to notify the controls that a load operation is
|
|
* complete. This allows the controls to recalculate play/paused state, which
|
|
* is important for platforms like Android where autoplay is disabled.
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.loadComplete = function() {
|
|
// If we are on Android or if autoplay is false, video.paused should be true.
|
|
// Otherwise, video.paused is false and the content is autoplaying.
|
|
this.onPlayStateChange_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Enable or disable the custom controls. Enabling disables native
|
|
* browser controls.
|
|
*
|
|
* @param {boolean} enabled
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.setEnabledShakaControls = function(enabled) {
|
|
this.enabled_ = enabled;
|
|
if (enabled) {
|
|
shaka.ui.Utils.setDisplay(
|
|
this.controlsButtonPanel_.parentElement, true);
|
|
|
|
// If we're hiding native controls, make sure the video element itself is
|
|
// not tab-navigable. Our custom controls will still be tab-navigable.
|
|
this.video_.tabIndex = -1;
|
|
this.video_.controls = false;
|
|
} else {
|
|
shaka.ui.Utils.setDisplay(
|
|
this.controlsButtonPanel_.parentElement, false);
|
|
}
|
|
|
|
// The effects of play state changes are inhibited while showing native
|
|
// browser controls. Recalculate that state now.
|
|
this.onPlayStateChange_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Enable or disable native browser controls. Enabling disables shaka
|
|
* controls.
|
|
*
|
|
* @param {boolean} enabled
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.setEnabledNativeControls = function(enabled) {
|
|
// If we enable the native controls, the element must be tab-navigable.
|
|
// If we disable the native controls, we want to make sure that the video
|
|
// element itself is not tab-navigable, so that the element is skipped over
|
|
// when tabbing through the page.
|
|
this.video_.controls = enabled;
|
|
this.video_.tabIndex = enabled ? 0 : -1;
|
|
|
|
if (enabled) {
|
|
this.setEnabledShakaControls(false);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @export
|
|
* @return {!shaka.cast.CastProxy}
|
|
*/
|
|
shaka.ui.Controls.prototype.getCastProxy = function() {
|
|
return this.castProxy_;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {shaka.ui.Localization}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.getLocalization = function() {
|
|
return this.localization_;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {!HTMLElement}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.getVideoContainer = function() {
|
|
return this.videoContainer_;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {!HTMLMediaElement}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.getVideo = function() {
|
|
return this.video_;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {!HTMLMediaElement}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.getLocalVideo = function() {
|
|
return this.localVideo_;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {!shaka.Player}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.getPlayer = function() {
|
|
return this.player_;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {!HTMLElement}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.getControlsContainer = function() {
|
|
return this.controlsContainer_;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {!shaka.extern.UIConfiguration}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.getConfig = function() {
|
|
return this.config_;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {boolean}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.isSeeking = function() {
|
|
return this.isSeeking_;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {boolean}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.isCastAllowed = function() {
|
|
return this.castAllowed_;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {number}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.getDisplayTime = function() {
|
|
const displayTime = this.isSeeking_ ?
|
|
Number(this.seekBar_.value) :
|
|
Number(this.video_.currentTime);
|
|
|
|
return displayTime;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {?number} time
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.setLastTouchEventTime = function(time) {
|
|
this.lastTouchEventTime_ = time;
|
|
};
|
|
|
|
|
|
/**
|
|
* Display controls even if css says overwise.
|
|
* Normally, controls opacity is controled by CSS, but there are
|
|
* a few special cases where we want controls to be displayed no
|
|
* matter what. For example, if the focus is on one of the settings
|
|
* menus. This method is called when we want to signal an exception
|
|
* to normal CSS opacity rules and keep the controls visible.
|
|
*
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.overrideCssShowControls = function() {
|
|
this.overrideCssShowControls_ = true;
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {boolean}
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.anySettingsMenusAreOpen = function() {
|
|
return this.settingsMenus_.some(
|
|
(menu) => menu.classList.contains('shaka-displayed'));
|
|
};
|
|
|
|
|
|
/**
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.hideSettingsMenus = function() {
|
|
this.hideSettingsMenusTimer_.tickNow();
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.updateLocalizedStrings_ = function() {
|
|
const LocIds = shaka.ui.Locales.Ids;
|
|
|
|
if (this.seekBar_) {
|
|
this.seekBar_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
|
|
this.localization_.resolve(LocIds.SEEK));
|
|
}
|
|
|
|
// Localize state-dependant labels
|
|
const makePlayNotPause = this.video_.paused && !this.isSeeking_;
|
|
const playButtonAriaLabelId = makePlayNotPause ? LocIds.PLAY : LocIds.PAUSE;
|
|
this.playButton_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
|
|
this.localization_.resolve(playButtonAriaLabelId));
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.initOptionalElementsToNull_ = function() {
|
|
// TODO: encapsulate/abstract range inputs and their containers
|
|
|
|
/** @private {HTMLElement} */
|
|
this.seekBarContainer_ = null;
|
|
|
|
/** @private {HTMLInputElement} */
|
|
this.seekBar_ = null;
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.createDOM_ = function() {
|
|
this.initOptionalElementsToNull_();
|
|
|
|
this.videoContainer_.classList.add('shaka-video-container');
|
|
this.video_.classList.add('shaka-video');
|
|
|
|
this.addControlsContainer_();
|
|
|
|
this.addPlayButton_();
|
|
|
|
this.addBufferingSpinner_();
|
|
|
|
this.addControlsButtonPanel_();
|
|
|
|
// Seek bar
|
|
if (this.config_.addSeekBar) {
|
|
this.addSeekBar_();
|
|
}
|
|
|
|
/** @private {!Array.<!Element>} */
|
|
this.settingsMenus_ = Array.from(
|
|
this.videoContainer_.getElementsByClassName('shaka-settings-menu'));
|
|
|
|
// Settings menus need to be positioned lower, if the seekbar is absent.
|
|
if (!this.seekBar_) {
|
|
for (let menu of this.settingsMenus_) {
|
|
menu.classList.add('shaka-low-position');
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addControlsContainer_ = function() {
|
|
/** @private {!HTMLElement} */
|
|
this.controlsContainer_ = shaka.util.Dom.createHTMLElement('div');
|
|
this.controlsContainer_.classList.add('shaka-controls-container');
|
|
this.videoContainer_.appendChild(this.controlsContainer_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addPlayButton_ = function() {
|
|
/** @private {!HTMLElement} */
|
|
this.playButtonContainer_ = shaka.util.Dom.createHTMLElement('div');
|
|
this.playButtonContainer_.classList.add('shaka-play-button-container');
|
|
this.controlsContainer_.appendChild(this.playButtonContainer_);
|
|
|
|
/** @private {!HTMLElement} */
|
|
this.playButton_ = shaka.util.Dom.createHTMLElement('button');
|
|
this.playButton_.classList.add('shaka-play-button');
|
|
this.playButton_.setAttribute('icon', 'play');
|
|
this.playButtonContainer_.appendChild(this.playButton_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addBufferingSpinner_ = function() {
|
|
goog.asserts.assert(this.playButtonContainer_,
|
|
'Must have play button container before spinner!');
|
|
|
|
// Svg elements have to be created with the svg xml namespace.
|
|
const xmlns = 'http://www.w3.org/2000/svg';
|
|
|
|
/** @private {!HTMLElement} */
|
|
this.bufferingSpinner_ =
|
|
/** @type {!HTMLElement} */(document.createElementNS(xmlns, 'svg'));
|
|
// NOTE: SVG elements do not have a classList on IE, so use setAttribute.
|
|
this.bufferingSpinner_.setAttribute('class', 'shaka-spinner-svg');
|
|
this.bufferingSpinner_.setAttribute('viewBox', '0 0 30 30');
|
|
this.playButton_.appendChild(this.bufferingSpinner_);
|
|
|
|
// These coordinates are relative to the SVG viewBox above. This is distinct
|
|
// from the actual display size in the page, since the "S" is for "Scalable."
|
|
// The radius of 14.5 is so that the edges of the 1-px-wide stroke will touch
|
|
// the edges of the viewBox.
|
|
const spinnerCircle = document.createElementNS(xmlns, 'circle');
|
|
spinnerCircle.setAttribute('class', 'shaka-spinner-path');
|
|
spinnerCircle.setAttribute('cx', '15');
|
|
spinnerCircle.setAttribute('cy', '15');
|
|
spinnerCircle.setAttribute('r', '14.5');
|
|
spinnerCircle.setAttribute('fill', 'none');
|
|
spinnerCircle.setAttribute('stroke-width', '1');
|
|
spinnerCircle.setAttribute('stroke-miterlimit', '10');
|
|
this.bufferingSpinner_.appendChild(spinnerCircle);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addControlsButtonPanel_ = function() {
|
|
/** @private {!HTMLElement} */
|
|
this.controlsButtonPanel_ = shaka.util.Dom.createHTMLElement('div');
|
|
this.controlsButtonPanel_.classList.add('shaka-controls-button-panel');
|
|
this.controlsButtonPanel_.classList.add('shaka-no-propagation');
|
|
this.controlsButtonPanel_.classList.add('shaka-show-controls-on-mouse-over');
|
|
this.controlsContainer_.appendChild(this.controlsButtonPanel_);
|
|
|
|
// Create the elements specified by controlPanelElements
|
|
for (let i = 0; i < this.config_.controlPanelElements.length; i++) {
|
|
const name = this.config_.controlPanelElements[i];
|
|
if (shaka.ui.ControlsPanel.elementNamesToFactories_.get(name)) {
|
|
const factory = shaka.ui.ControlsPanel.elementNamesToFactories_.get(name);
|
|
this.elements_.push(factory.create(this.controlsButtonPanel_, this));
|
|
} else {
|
|
shaka.log.alwaysWarn('Unrecognized control panel element requested:',
|
|
name);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addEventListeners_ = function() {
|
|
// TODO: Convert adding event listers to the "() =>" form.
|
|
|
|
this.player_.addEventListener('buffering', () => {
|
|
this.onBufferingStateChange_();
|
|
});
|
|
// Set the initial state, as well.
|
|
this.onBufferingStateChange_();
|
|
|
|
// Listen for key down events to detect tab and enable outline
|
|
// for focused elements.
|
|
this.eventManager_.listen(window, 'keydown', this.onKeyDown_.bind(this));
|
|
|
|
this.video_.addEventListener(
|
|
'play', this.onPlayStateChange_.bind(this));
|
|
this.video_.addEventListener(
|
|
'pause', this.onPlayStateChange_.bind(this));
|
|
|
|
// Since videos go into a paused state at the end, Chrome and Edge both fire
|
|
// the 'pause' event when a video ends. IE 11 only fires the 'ended' event.
|
|
this.video_.addEventListener(
|
|
'ended', this.onPlayStateChange_.bind(this));
|
|
|
|
if (this.seekBar_) {
|
|
this.seekBar_.addEventListener(
|
|
'mousedown', this.onSeekStart_.bind(this));
|
|
this.seekBar_.addEventListener(
|
|
'touchstart', this.onSeekStart_.bind(this), {passive: true});
|
|
this.seekBar_.addEventListener(
|
|
'input', this.onSeekInput_.bind(this));
|
|
this.seekBar_.addEventListener(
|
|
'touchend', this.onSeekEnd_.bind(this));
|
|
this.seekBar_.addEventListener(
|
|
'mouseup', this.onSeekEnd_.bind(this));
|
|
}
|
|
|
|
this.controlsContainer_.addEventListener(
|
|
'touchstart', this.onContainerTouch_.bind(this), {passive: false});
|
|
this.controlsContainer_.addEventListener(
|
|
'click', this.onContainerClick_.bind(this));
|
|
|
|
// Elements that should not propagate clicks (controls panel, menus)
|
|
const noPropagationElements = this.videoContainer_.getElementsByClassName(
|
|
'shaka-no-propagation');
|
|
for (let i = 0; i < noPropagationElements.length; i++) {
|
|
let element = noPropagationElements[i];
|
|
element.addEventListener(
|
|
'click', function(event) { event.stopPropagation(); });
|
|
}
|
|
|
|
// Keep showing controls if one of those elements is hovered
|
|
let showControlsElements = this.videoContainer_.getElementsByClassName(
|
|
'shaka-show-controls-on-mouse-over');
|
|
for (let i = 0; i < showControlsElements.length; i++) {
|
|
let element = showControlsElements[i];
|
|
element.addEventListener(
|
|
'mouseover', () => {
|
|
this.overrideCssShowControls_ = true;
|
|
});
|
|
|
|
element.addEventListener(
|
|
'mouseleave', () => {
|
|
this.overrideCssShowControls_ = false;
|
|
});
|
|
}
|
|
|
|
this.videoContainer_.addEventListener(
|
|
'mousemove', this.onMouseMove_.bind(this));
|
|
this.videoContainer_.addEventListener(
|
|
'touchmove', this.onMouseMove_.bind(this), {passive: true});
|
|
this.videoContainer_.addEventListener(
|
|
'touchend', this.onMouseMove_.bind(this), {passive: true});
|
|
this.videoContainer_.addEventListener(
|
|
'mouseleave', this.onMouseLeave_.bind(this));
|
|
|
|
// Overflow menus are supposed to hide once you click elsewhere
|
|
// on the video element. The code in onContainerClick_ ensures that.
|
|
// However, clicks on controls panel don't propagate to the container,
|
|
// so we have to explicitly hide the menus onclick here.
|
|
this.controlsButtonPanel_.addEventListener('click', () => {
|
|
this.hideSettingsMenusTimer_.tickNow();
|
|
});
|
|
|
|
this.castProxy_.addEventListener(
|
|
'caststatuschanged', (e) => {
|
|
this.onCastStatusChange_(e);
|
|
});
|
|
|
|
this.videoContainer_.addEventListener('keyup', this.onKeyUp_.bind(this));
|
|
|
|
this.localization_.addEventListener(
|
|
shaka.ui.Localization.LOCALE_UPDATED,
|
|
(e) => this.updateLocalizedStrings_());
|
|
|
|
this.localization_.addEventListener(
|
|
shaka.ui.Localization.LOCALE_CHANGED,
|
|
(e) => this.updateLocalizedStrings_());
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addSeekBar_ = function() {
|
|
// This container is to support IE 11. See detailed notes in
|
|
// less/range_elements.less for a complete explanation.
|
|
// TODO: Factor this into a range-element component.
|
|
this.seekBarContainer_ = shaka.util.Dom.createHTMLElement('div');
|
|
this.seekBarContainer_.classList.add('shaka-seek-bar-container');
|
|
|
|
this.seekBar_ =
|
|
/** @type {!HTMLInputElement} */ (document.createElement('input'));
|
|
this.seekBar_.classList.add('shaka-seek-bar');
|
|
this.seekBar_.type = 'range';
|
|
// NOTE: step=any causes keyboard nav problems on IE 11.
|
|
this.seekBar_.setAttribute('step', 'any');
|
|
this.seekBar_.setAttribute('min', '0');
|
|
this.seekBar_.setAttribute('max', '1');
|
|
this.seekBar_.value = '0';
|
|
this.seekBar_.classList.add('shaka-no-propagation');
|
|
this.seekBar_.classList.add('shaka-show-controls-on-mouse-over');
|
|
|
|
this.seekBarContainer_.appendChild(this.seekBar_);
|
|
this.controlsContainer_.appendChild(this.seekBarContainer_);
|
|
};
|
|
|
|
|
|
/**
|
|
* Hiding the cursor when the mouse stops moving seems to be the only decent UX
|
|
* in fullscreen mode. Since we can't use pure CSS for that, we use events both
|
|
* in and out of fullscreen mode.
|
|
* Showing the control bar when a key is pressed, and hiding it after some time.
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onMouseMove_ = function(event) {
|
|
// Disable blue outline for focused elements for mouse navigation.
|
|
if (event.type == 'mousemove') {
|
|
this.controlsContainer_.classList.remove('shaka-keyboard-navigation');
|
|
}
|
|
if (event.type == 'touchstart' || event.type == 'touchmove' ||
|
|
event.type == 'touchend' || event.type == 'keyup') {
|
|
this.lastTouchEventTime_ = Date.now();
|
|
} else if (this.lastTouchEventTime_ + 1000 < Date.now()) {
|
|
// It has been a while since the last touch event, this is probably a real
|
|
// mouse moving, so treat it like a mouse.
|
|
this.lastTouchEventTime_ = null;
|
|
}
|
|
|
|
// When there is a touch, we can get a 'mousemove' event after touch events.
|
|
// This should be treated as part of the touch, which has already been handled
|
|
if (this.lastTouchEventTime_ && event.type == 'mousemove') {
|
|
return;
|
|
}
|
|
|
|
// Use the cursor specified in the CSS file.
|
|
this.videoContainer_.style.cursor = '';
|
|
|
|
// Make sure we are not about to hide the settings menus and then force them
|
|
// open.
|
|
this.hideSettingsMenusTimer_.stop();
|
|
this.setControlsOpacity_(shaka.ui.Enums.Opacity.OPAQUE);
|
|
this.updateTimeAndSeekRange_();
|
|
|
|
// Hide the cursor when the mouse stops moving.
|
|
// Only applies while the cursor is over the video container.
|
|
this.mouseStillTimer_.stop();
|
|
|
|
// Only start a timeout on 'touchend' or for 'mousemove' with no touch events.
|
|
if (event.type == 'touchend' ||
|
|
event.type == 'keyup'|| !this.lastTouchEventTime_) {
|
|
this.mouseStillTimer_.tickAfter(/* seconds= */ 3);
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onMouseLeave_ = function() {
|
|
// We sometimes get 'mouseout' events with touches. Since we can never leave
|
|
// the video element when touching, ignore.
|
|
if (this.lastTouchEventTime_) return;
|
|
|
|
// Stop the timer and invoke the callback now to hide the controls. If we
|
|
// don't, the opacity style we set in onMouseMove_ will continue to override
|
|
// the opacity in CSS and force the controls to stay visible.
|
|
this.mouseStillTimer_.tickNow();
|
|
};
|
|
|
|
|
|
/**
|
|
* This callback is for when we are pretty sure that the mouse has stopped
|
|
* moving (aka the mouse is still). This method should only be called via
|
|
* |mouseStillTimer_|. If this behaviour needs to be invoked directly, use
|
|
* |mouseStillTimer_.tickNow()|.
|
|
*
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onMouseStill_ = function() {
|
|
// Hide the cursor. (NOTE: not supported on IE)
|
|
this.videoContainer_.style.cursor = 'none';
|
|
|
|
// Keep showing the controls if video is paused or one of the control menus
|
|
// is hovered.
|
|
if ((this.video_.paused && !this.isSeeking_) ||
|
|
this.overrideCssShowControls_) {
|
|
this.setControlsOpacity_(shaka.ui.Enums.Opacity.OPAQUE);
|
|
} else {
|
|
this.setControlsOpacity_(shaka.ui.Enums.Opacity.TRANSPARENT);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onContainerTouch_ = function(event) {
|
|
if (!this.video_.duration) {
|
|
// Can't play yet. Ignore.
|
|
return;
|
|
}
|
|
|
|
if (this.isOpaque_()) {
|
|
this.lastTouchEventTime_ = Date.now();
|
|
// The controls are showing.
|
|
// Let this event continue and become a click.
|
|
} else {
|
|
// The controls are hidden, so show them.
|
|
this.onMouseMove_(event);
|
|
// Stop this event from becoming a click event.
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onContainerClick_ = function(event) {
|
|
if (!this.enabled_) return;
|
|
|
|
if (this.anySettingsMenusAreOpen()) {
|
|
this.hideSettingsMenusTimer_.tickNow();
|
|
} else {
|
|
this.onPlayPauseClick_();
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onPlayPauseClick_ = function() {
|
|
if (!this.enabled_) return;
|
|
|
|
if (!this.video_.duration) {
|
|
// Can't play yet. Ignore.
|
|
return;
|
|
}
|
|
|
|
this.player_.cancelTrickPlay();
|
|
|
|
if (this.video_.paused) {
|
|
this.video_.play();
|
|
} else {
|
|
this.video_.pause();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onCastStatusChange_ = function(event) {
|
|
const isCasting = this.castProxy_.isCasting();
|
|
this.dispatchEvent(new shaka.util.FakeEvent('caststatuschanged', {
|
|
newStatus: isCasting,
|
|
}));
|
|
|
|
if (isCasting) {
|
|
this.controlsContainer_.setAttribute('casting', 'true');
|
|
} else {
|
|
this.controlsContainer_.removeAttribute('casting');
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onPlayStateChange_ = function() {
|
|
// On IE 11, a video may end without going into a paused state. To correct
|
|
// both the UI state and the state of the video tag itself, we explicitly
|
|
// pause the video if that happens.
|
|
if (this.video_.ended && !this.video_.paused) {
|
|
this.video_.pause();
|
|
}
|
|
|
|
// Video is paused during seek, so don't show the play arrow while seeking:
|
|
if (this.enabled_ && this.video_.paused && !this.isSeeking_) {
|
|
this.playButton_.setAttribute('icon', 'play');
|
|
this.playButton_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
|
|
this.localization_.resolve(shaka.ui.Locales.Ids.PLAY));
|
|
} else {
|
|
this.playButton_.setAttribute('icon', 'pause');
|
|
this.playButton_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
|
|
this.localization_.resolve(shaka.ui.Locales.Ids.PAUSE));
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onSeekStart_ = function() {
|
|
if (!this.enabled_) return;
|
|
|
|
this.isSeeking_ = true;
|
|
this.video_.pause();
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onSeekInput_ = function() {
|
|
if (!this.enabled_) return;
|
|
|
|
if (!this.video_.duration) {
|
|
// Can't seek yet. Ignore.
|
|
return;
|
|
}
|
|
|
|
// Update the UI right away.
|
|
this.updateTimeAndSeekRange_();
|
|
|
|
// We want to wait until the user has stopped moving the seek bar for a
|
|
// little bit to avoid the number of times we ask the player to seek.
|
|
//
|
|
// To do this, we will start a timer that will fire in a little bit, but if
|
|
// we see another seek bar change, we will cancel that timer and re-start it.
|
|
//
|
|
// Calling |start| on an already pending timer will cancel the old request
|
|
// and start the new one.
|
|
this.seekTimer_.tickAfter(/* seconds= */ 0.125);
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onSeekEnd_ = function() {
|
|
if (!this.enabled_) return;
|
|
|
|
// They just let go of the seek bar, so cancel the timer and manually
|
|
// call the event so that we can respond immediately.
|
|
this.seekTimer_.tickNow();
|
|
|
|
this.isSeeking_ = false;
|
|
this.video_.play();
|
|
};
|
|
|
|
|
|
/**
|
|
* Support controls with keyboard inputs.
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onKeyUp_ = function(event) {
|
|
let key = event.key;
|
|
|
|
let activeElement = document.activeElement;
|
|
let isVolumeBar = activeElement && activeElement.classList ?
|
|
activeElement.classList.contains('shaka-volume-bar') : false;
|
|
let isSeekBar = activeElement && activeElement.classList &&
|
|
activeElement.classList.contains('shaka-seek-bar');
|
|
// Show the control panel if it is on focus or any button is pressed.
|
|
if (this.controlsContainer_.contains(activeElement)) {
|
|
this.onMouseMove_(event);
|
|
}
|
|
|
|
// When the key is released, remove it from the pressed keys set.
|
|
this.pressedKeys_.delete(event.keyCode);
|
|
|
|
|
|
switch (key) {
|
|
case 'ArrowLeft':
|
|
// If it's not focused on the volume bar, move the seek time backward
|
|
// for 5 sec. Otherwise, the volume will be adjusted automatically.
|
|
if (!isVolumeBar) {
|
|
this.seek_(this.video_.currentTime - 5, event);
|
|
}
|
|
break;
|
|
case 'ArrowRight':
|
|
// If it's not focused on the volume bar, move the seek time forward
|
|
// for 5 sec. Otherwise, the volume will be adjusted automatically.
|
|
if (!isVolumeBar) {
|
|
this.seek_(this.video_.currentTime + 5, event);
|
|
}
|
|
break;
|
|
// Jump to the beginning of the video's seek range.
|
|
case 'Home':
|
|
this.seek_(this.player_.seekRange().start, event);
|
|
break;
|
|
// Jump to the end of the video's seek range.
|
|
case 'End':
|
|
this.seek_(this.player_.seekRange().end, event);
|
|
break;
|
|
// Pause or play by pressing space on the seek bar.
|
|
case ' ':
|
|
if (isSeekBar) {
|
|
this.onPlayPauseClick_();
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Called both as an event listener and directly by the controls to initialize
|
|
* the buffering state.
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onBufferingStateChange_ = function() {
|
|
// Don't use setDisplay_ here, since the SVG spinner doesn't have classList
|
|
// on IE.
|
|
if (this.player_.isBuffering()) {
|
|
this.bufferingSpinner_.setAttribute(
|
|
'class', 'shaka-spinner-svg');
|
|
} else {
|
|
this.bufferingSpinner_.setAttribute(
|
|
'class', 'shaka-spinner-svg shaka-hidden');
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {boolean}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.isOpaque_ = function() {
|
|
if (!this.enabled_) return false;
|
|
|
|
// TODO: refactor into a single property
|
|
// While you are casting, the UI is always opaque.
|
|
if (this.castProxy_ && this.castProxy_.isCasting()) return true;
|
|
|
|
return this.controlsContainer_.getAttribute('shown') != null;
|
|
};
|
|
|
|
|
|
/**
|
|
* Update the video's current time based on the keyboard operations.
|
|
* @param {number} currentTime
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.seek_ = function(currentTime, event) {
|
|
this.video_.currentTime = currentTime;
|
|
this.updateTimeAndSeekRange_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Called when the seek range or current time need to be updated.
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.updateTimeAndSeekRange_ = function() {
|
|
// Suppress updates if the controls are hidden.
|
|
if (!this.isOpaque_()) {
|
|
return;
|
|
}
|
|
|
|
const Constants = shaka.ui.Constants;
|
|
let displayTime = this.isSeeking_ ?
|
|
Number(this.seekBar_.value) :
|
|
Number(this.video_.currentTime);
|
|
let bufferedLength = this.video_.buffered.length;
|
|
let bufferedStart = bufferedLength ? this.video_.buffered.start(0) : 0;
|
|
let bufferedEnd =
|
|
bufferedLength ? this.video_.buffered.end(bufferedLength - 1) : 0;
|
|
let seekRange = this.player_.seekRange();
|
|
let seekRangeSize = seekRange.end - seekRange.start;
|
|
|
|
if (this.seekBar_) {
|
|
this.seekBar_.min = seekRange.start;
|
|
this.seekBar_.max = seekRange.end;
|
|
}
|
|
|
|
if (this.player_.isLive()) {
|
|
// The amount of time we are behind the live edge.
|
|
let behindLive = Math.floor(seekRange.end - displayTime);
|
|
displayTime = Math.max(0, behindLive);
|
|
|
|
if (!this.isSeeking_ && this.seekBar_) {
|
|
this.seekBar_.value = seekRange.end - displayTime;
|
|
}
|
|
} else {
|
|
if (!this.isSeeking_ && this.seekBar_) {
|
|
this.seekBar_.value = displayTime;
|
|
}
|
|
}
|
|
|
|
if (this.seekBar_) {
|
|
// Hide seekbar seek window is very small.
|
|
const seekRange = this.player_.seekRange();
|
|
const seekWindow = seekRange.end - seekRange.start;
|
|
if (seekWindow < Constants.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR) {
|
|
shaka.ui.Utils.setDisplay(this.seekBarContainer_, false);
|
|
for (let menu of this.settingsMenus_) {
|
|
menu.classList.add('shaka-low-position');
|
|
}
|
|
} else {
|
|
shaka.ui.Utils.setDisplay(this.seekBarContainer_, true);
|
|
for (let menu of this.settingsMenus_) {
|
|
menu.classList.remove('shaka-low-position');
|
|
}
|
|
|
|
let gradient = ['to right'];
|
|
if (bufferedLength == 0) {
|
|
gradient.push('#000 0%');
|
|
} else {
|
|
const clampedBufferStart = Math.max(bufferedStart, seekRange.start);
|
|
const clampedBufferEnd = Math.min(bufferedEnd, seekRange.end);
|
|
|
|
const bufferStartDistance = clampedBufferStart - seekRange.start;
|
|
const bufferEndDistance = clampedBufferEnd - seekRange.start;
|
|
const playheadDistance = displayTime - seekRange.start;
|
|
|
|
// NOTE: the fallback to zero eliminates NaN.
|
|
const bufferStartFraction = (bufferStartDistance / seekRangeSize) || 0;
|
|
const bufferEndFraction = (bufferEndDistance / seekRangeSize) || 0;
|
|
const playheadFraction = (playheadDistance / seekRangeSize) || 0;
|
|
|
|
gradient.push(Constants.SEEK_BAR_BASE_COLOR + ' ' +
|
|
(bufferStartFraction * 100) + '%');
|
|
gradient.push(Constants.SEEK_BAR_PLAYED_COLOR + ' ' +
|
|
(bufferStartFraction * 100) + '%');
|
|
gradient.push(Constants.SEEK_BAR_PLAYED_COLOR + ' ' +
|
|
(playheadFraction * 100) + '%');
|
|
gradient.push(Constants.SEEK_BAR_BUFFERED_COLOR + ' ' +
|
|
(playheadFraction * 100) + '%');
|
|
gradient.push(Constants.SEEK_BAR_BUFFERED_COLOR + ' ' +
|
|
(bufferEndFraction * 100) + '%');
|
|
gradient.push(Constants.SEEK_BAR_BASE_COLOR + ' ' +
|
|
(bufferEndFraction * 100) + '%');
|
|
}
|
|
this.seekBarContainer_.style.background =
|
|
'linear-gradient(' + gradient.join(',') + ')';
|
|
}
|
|
}
|
|
|
|
this.dispatchEvent(new shaka.util.FakeEvent('timeandseekrangeupdated'));
|
|
};
|
|
|
|
|
|
/**
|
|
* Add behaviors for keyboard navigation.
|
|
* 1. Add blue outline for focused elements.
|
|
* 2. Allow exiting overflow settings menus by pressing Esc key.
|
|
* 3. When navigating on overflow settings menu by pressing Tab
|
|
* key or Shift+Tab keys keep the focus inside overflow menu.
|
|
*
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onKeyDown_ = function(event) {
|
|
// Add the key code to the pressed keys set when it's pressed.
|
|
this.pressedKeys_.add(event.keyCode);
|
|
|
|
const anySettingsMenusAreOpen = this.anySettingsMenusAreOpen();
|
|
|
|
if (event.keyCode == shaka.ui.Constants.KEYCODE_TAB) {
|
|
// Enable blue outline for focused elements for keyboard
|
|
// navigation.
|
|
this.controlsContainer_.classList.add('shaka-keyboard-navigation');
|
|
this.eventManager_.listen(window, 'mousedown',
|
|
this.onMouseDown_.bind(this));
|
|
}
|
|
|
|
// If escape key was pressed, close any open settings menus.
|
|
if (event.keyCode == shaka.ui.Constants.KEYCODE_ESCAPE) {
|
|
this.hideSettingsMenusTimer_.tickNow();
|
|
}
|
|
|
|
if (anySettingsMenusAreOpen &&
|
|
this.pressedKeys_.has(shaka.ui.Constants.KEYCODE_TAB)) {
|
|
// If Tab key or Shift+Tab keys are pressed when navigating through
|
|
// an overflow settings menu, keep the focus to loop inside the
|
|
// overflow menu.
|
|
this.keepFocusInMenu_(event);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* When the user is using keyboard to navigate inside the overflow settings
|
|
* menu (pressing Tab key to go forward, or pressing Shift + Tab keys to go
|
|
* backward), make sure it's focused only on the elements of the overflow
|
|
* panel.
|
|
* This is called by onKeyDown_() function, when there's a settings overflow
|
|
* menu open, and the Tab key / Shift+Tab keys are pressed.
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.keepFocusInMenu_ = function(event) {
|
|
const openSettingsMenus = this.settingsMenus_.filter(
|
|
(menu) => menu.classList.contains('shaka-displayed'));
|
|
const settingsMenu = openSettingsMenus[0];
|
|
if (settingsMenu.childNodes.length) {
|
|
// Get the first and the last displaying child element from the overflow
|
|
// menu.
|
|
let firstShownChild = settingsMenu.firstElementChild;
|
|
while (firstShownChild &&
|
|
firstShownChild.classList.contains('shaka-hidden')) {
|
|
firstShownChild = firstShownChild.nextElementSibling;
|
|
}
|
|
|
|
let lastShownChild = settingsMenu.lastElementChild;
|
|
while (lastShownChild &&
|
|
lastShownChild.classList.contains('shaka-hidden')) {
|
|
lastShownChild = lastShownChild.previousElementSibling;
|
|
}
|
|
|
|
const activeElement = document.activeElement;
|
|
// When only Tab key is pressed, navigate to the next elememnt.
|
|
// If it's currently focused on the last shown child element of the
|
|
// overflow menu, let the focus move to the first child element of the
|
|
// menu.
|
|
// When Tab + Shift keys are pressed at the same time, navigate to the
|
|
// previous element. If it's currently focused on the first shown child
|
|
// element of the overflow menu, let the focus move to the last child
|
|
// element of the menu.
|
|
if (this.pressedKeys_.has(shaka.ui.Constants.KEYCODE_SHIFT)) {
|
|
if (activeElement == firstShownChild) {
|
|
event.preventDefault();
|
|
lastShownChild.focus();
|
|
}
|
|
} else {
|
|
if (activeElement == lastShownChild) {
|
|
event.preventDefault();
|
|
firstShownChild.focus();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Removes class for keyboard navigation if mouse navigation
|
|
* is active.
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onMouseDown_ = function() {
|
|
this.eventManager_.unlisten(window, 'mousedown');
|
|
this.eventManager_.listen(window, 'keydown', this.onKeyDown_.bind(this));
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!shaka.ui.Enums.Opacity} opacity
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.setControlsOpacity_ = function(opacity) {
|
|
if (opacity == shaka.ui.Enums.Opacity.OPAQUE) {
|
|
this.controlsContainer_.setAttribute('shown', 'true');
|
|
} else {
|
|
this.controlsContainer_.removeAttribute('shown');
|
|
// If there's an overflow menu open, keep it this way for a couple of
|
|
// seconds in case a user immediately initiates another mouse move to
|
|
// interact with the menus. If that didn't happen, go ahead and hide
|
|
// the menus.
|
|
this.hideSettingsMenusTimer_.tickAfter(/* seconds= */ 2);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Create a localization instance already pre-loaded with all the locales that
|
|
* we support.
|
|
*
|
|
* @return {!shaka.ui.Localization}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.createLocalization_ = function() {
|
|
/** @type {string} */
|
|
const fallbackLocale = 'en';
|
|
|
|
/** @type {!shaka.ui.Localization} */
|
|
const localization = new shaka.ui.Localization(fallbackLocale);
|
|
shaka.ui.Locales.apply(localization);
|
|
localization.changeLocale(navigator.languages || []);
|
|
|
|
return localization;
|
|
};
|