mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-19 16:47:01 +03:00
9e180712f4
Updated the shaka.util.Timer class to ES6. In this upgrade, the implementation was slightly changed to make better use of arrow-functions. Documentation for the class was expanded on to better document how multiple calls to start would work. To ensure consistency, both "schedule*" methods were merged into a single "start" method. Issue #1157 Change-Id: Iae86cae4d9cb751f0985ef20c371c0023c40bd53
2722 lines
82 KiB
JavaScript
2722 lines
82 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.require('goog.asserts');
|
|
goog.require('mozilla.LanguageMapping');
|
|
goog.require('shaka.ui.Locales');
|
|
goog.require('shaka.ui.Localization');
|
|
goog.require('shaka.ui.Utils');
|
|
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 {!Map.<string, !Function>} */
|
|
this.elementNamesToFunctions_ = new Map([
|
|
['time_and_duration', () => { this.addCurrentTime_(); }],
|
|
['mute', () => { this.addMuteButton_(); }],
|
|
['volume', () => { this.addVolumeBar_(); }],
|
|
['fullscreen', () => { this.addFullscreenButton_(); }],
|
|
['overflow_menu', () => { this.addOverflowMenuButton_(); }],
|
|
['captions', () => { this.addCaptionButton_(); }],
|
|
['cast', () => { this.addCastButton_(); }],
|
|
['rewind', () => { this.addRewindButton_(); }],
|
|
['fast_forward', () => { this.addFastForwardButton_(); }],
|
|
['quality', () => { this.addResolutionButton_(); }],
|
|
['language', () => { this.addLanguagesButton_(); }],
|
|
['picture_in_picture', () => { this.addPipButton_(); }],
|
|
]);
|
|
|
|
/** @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;
|
|
|
|
/** @private {number} */
|
|
this.trickPlayRate_ = 1;
|
|
|
|
/** @private {?number} */
|
|
this.seekTimeoutId_ = null;
|
|
|
|
/** @private {?number} */
|
|
this.mouseStillTimeoutId_ = null;
|
|
|
|
/** @private {?number} */
|
|
this.lastTouchEventTime_ = null;
|
|
|
|
/** @private {shaka.ui.Localization} */
|
|
this.localization_ = shaka.ui.Controls.createLocalization_();
|
|
|
|
this.createDOM_();
|
|
|
|
const LocIds = shaka.ui.Locales.Ids;
|
|
|
|
/** @private {!Map.<HTMLElement, string>} */
|
|
this.ariaLabels_ = new Map()
|
|
.set(this.seekBar_, LocIds.ARIA_LABEL_SEEK)
|
|
.set(this.captionButton_, LocIds.ARIA_LABEL_CAPTIONS)
|
|
.set(this.backFromCaptionsButton_, LocIds.ARIA_LABEL_BACK)
|
|
.set(this.backFromResolutionButton_, LocIds.ARIA_LABEL_BACK)
|
|
.set(this.backFromLanguageButton_, LocIds.ARIA_LABEL_BACK)
|
|
.set(this.rewindButton_, LocIds.ARIA_LABEL_REWIND)
|
|
.set(this.fastForwardButton_, LocIds.ARIA_LABEL_FAST_FORWARD)
|
|
.set(this.resolutionButton_, LocIds.ARIA_LABEL_RESOLUTION)
|
|
.set(this.languagesButton_, LocIds.ARIA_LABEL_LANGUAGE)
|
|
.set(this.castButton_, LocIds.ARIA_LABEL_CAST)
|
|
.set(this.volumeBar_, LocIds.ARIA_LABEL_VOLUME)
|
|
.set(this.overflowMenuButton_, LocIds.ARIA_LABEL_MORE_SETTINGS);
|
|
|
|
/** @private {!Map.<HTMLElement, string>} */
|
|
this.textContentToLocalize_ = new Map()
|
|
.set(this.captionsNameSpan_, LocIds.LABEL_CAPTIONS)
|
|
.set(this.backFromCaptionsSpan_, LocIds.LABEL_CAPTIONS)
|
|
.set(this.captionsOffSpan_, LocIds.LABEL_CAPTIONS_OFF)
|
|
.set(this.castNameSpan_, LocIds.LABEL_CAST)
|
|
.set(this.backFromResolutionSpan_, LocIds.LABEL_RESOLUTION)
|
|
.set(this.resolutionNameSpan_, LocIds.LABEL_RESOLUTION)
|
|
.set(this.abrOnSpan_, LocIds.LABEL_AUTO_QUALITY)
|
|
.set(this.languageNameSpan_, LocIds.LABEL_LANGUAGE)
|
|
.set(this.backFromLanguageSpan_, LocIds.LABEL_LANGUAGE)
|
|
.set(this.pipNameSpan_, LocIds.LABEL_PICTURE_IN_PICTURE);
|
|
|
|
this.updateLocalizedStrings_();
|
|
|
|
/** @private {shaka.util.Timer} */
|
|
this.timeAndSeekRangeTimer_ =
|
|
new shaka.util.Timer(() => this.updateTimeAndSeekRange_());
|
|
this.timeAndSeekRangeTimer_.start(
|
|
/* seconds= */ 0.125, /* repeating= */ true);
|
|
|
|
/** @private {shaka.util.EventManager} */
|
|
this.eventManager_ = new shaka.util.EventManager();
|
|
|
|
this.addEventListeners_();
|
|
|
|
this.hideSettingsMenusTimer_ =
|
|
new shaka.util.Timer(() => {
|
|
this.hideSettingsMenus_();
|
|
});
|
|
|
|
// Initialize caption state with a fake event.
|
|
this.onCaptionStateChange_();
|
|
|
|
// 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);
|
|
};
|
|
|
|
goog.inherits(shaka.ui.Controls, shaka.util.FakeEventTarget);
|
|
|
|
|
|
/**
|
|
* @override
|
|
* @export
|
|
*/
|
|
shaka.ui.Controls.prototype.destroy = function() {
|
|
/** @type {!Array.<!Promise>} */
|
|
const waitFor = [];
|
|
|
|
if (this.eventManager_) {
|
|
waitFor.push(this.eventManager_.destroy());
|
|
}
|
|
|
|
if (this.timeAndSeekRangeTimer_) {
|
|
this.timeAndSeekRangeTimer_.stop();
|
|
}
|
|
|
|
this.eventManager_ = null;
|
|
this.localization_ = null;
|
|
this.timeAndSeekRangeTimer_ = null;
|
|
|
|
return Promise.all(waitFor);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.updateLocalizedStrings_ = function() {
|
|
const Controls = shaka.ui.Controls;
|
|
const LocIds = shaka.ui.Locales.Ids;
|
|
|
|
// Localize aria labels
|
|
let elements = this.ariaLabels_.keys();
|
|
for (const element of elements) {
|
|
if (element == null) {
|
|
continue;
|
|
}
|
|
|
|
const id = this.ariaLabels_.get(element);
|
|
element.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(id));
|
|
}
|
|
|
|
// Localize state-dependant labels
|
|
const makePlayNotPause = this.video_.paused && !this.isSeeking_;
|
|
const playButtonAriaLabelId = makePlayNotPause ? LocIds.ARIA_LABEL_PLAY :
|
|
LocIds.ARIA_LABEL_PAUSE;
|
|
this.playButton_.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(playButtonAriaLabelId));
|
|
|
|
if (this.muteButton_) {
|
|
const muteButtonAriaLabelId = this.video_.muted ? LocIds.ARIA_LABEL_UNMUTE :
|
|
LocIds.ARIA_LABEL_MUTE;
|
|
this.muteButton_.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(muteButtonAriaLabelId));
|
|
}
|
|
|
|
if (this.fullscreenButton_) {
|
|
const fullscreenAriaLabel = document.fullscreenElement ?
|
|
LocIds.ARIA_LABEL_EXIT_FULL_SCREEN :
|
|
LocIds.ARIA_LABEL_FULL_SCREEN;
|
|
this.fullscreenButton_.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(fullscreenAriaLabel));
|
|
}
|
|
|
|
if (this.pipButton_) {
|
|
const pipAriaLabel = document.pictureInPictureElement ?
|
|
LocIds.ARIA_LABEL_EXIT_PICTURE_IN_PICTURE :
|
|
LocIds.ARIA_LABEL_ENTER_PICTURE_IN_PICTURE;
|
|
this.pipButton_.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(pipAriaLabel));
|
|
|
|
const currentPipState = document.pictureInPictureElement ?
|
|
LocIds.LABEL_PICTURE_IN_PICTURE_ON :
|
|
LocIds.LABEL_PICTURE_IN_PICTURE_OFF;
|
|
|
|
this.currentPipState_.textContent =
|
|
this.localization_.resolve(currentPipState);
|
|
}
|
|
|
|
// If we're not casting, string "not casting" will be displayed,
|
|
// which needs localization.
|
|
this.setCurrentCastSelection_();
|
|
|
|
// If we're at "auto" resolution, this string needs localization.
|
|
this.updateResolutionSelection_();
|
|
|
|
// If captions/subtitles are off, this string needs localization.
|
|
this.updateTextLanguages_();
|
|
|
|
// Localize text
|
|
elements = this.textContentToLocalize_.keys();
|
|
for (const element of elements) {
|
|
if (element == null) {
|
|
continue;
|
|
}
|
|
|
|
const id = this.textContentToLocalize_.get(element);
|
|
element.textContent = this.localization_.resolve(id);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @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 {HTMLElement} */
|
|
this.volumeBarContainer_ = null;
|
|
|
|
/** @private {HTMLInputElement} */
|
|
this.volumeBar_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.muteButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.captionButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.captionIcon_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.fullscreenButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.currentTime_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.castButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.castIcon_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.overflowMenuButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.rewindButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.fastForwardButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.resolutionButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.languagesButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.resolutionMenu_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.audioLangMenu_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.textLangMenu_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.currentResolution_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.castNameSpan_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.currentAudioLanguage_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.currentCaptions_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.captionsNameSpan_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.backFromCaptionsSpan_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.backFromResolutionButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.backFromLanguageButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.captionsOffSpan_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.castCurrentSelectionSpan_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.backFromResolutionSpan_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.resolutionNameSpan_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.languageNameSpan_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.backFromLanguageSpan_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.abrOnSpan_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.backFromCaptionsButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.pipButton_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.pipNameSpan_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.currentPipState_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.pipIcon_ = 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_();
|
|
|
|
// Overflow menu
|
|
// Adding the overflow menu after the controls button panel, since the
|
|
// screen reader follows DOM orders.
|
|
this.addOverflowMenu_();
|
|
|
|
// Seek bar
|
|
if (this.config_.addSeekBar) {
|
|
this.addSeekBar_();
|
|
}
|
|
|
|
/** @private {!NodeList.<!Element>} */
|
|
this.backToOverflowMenuButtons_ = this.videoContainer_.
|
|
getElementsByClassName('shaka-back-to-overflow-button');
|
|
|
|
/** @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.ui.Controls.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.ui.Controls.createHTMLElement_('div');
|
|
this.playButtonContainer_.classList.add('shaka-play-button-container');
|
|
this.controlsContainer_.appendChild(this.playButtonContainer_);
|
|
|
|
/** @private {!HTMLElement} */
|
|
this.playButton_ = shaka.ui.Controls.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.ui.Controls.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 (this.elementNamesToFunctions_.get(name)) {
|
|
if (shaka.ui.Controls.controlPanelElements_.indexOf(name) == -1) {
|
|
// Not a control panel element, skip
|
|
shaka.log.warning('Element is not part of control panel ' +
|
|
'elements and will be skipped', name);
|
|
continue;
|
|
}
|
|
this.elementNamesToFunctions_.get(name)();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addEventListeners_ = function() {
|
|
// TODO: Convert adding event listers to the "() =>" form.
|
|
|
|
this.player_.addEventListener(
|
|
'buffering', this.onBufferingStateChange_.bind(this));
|
|
|
|
// 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));
|
|
}
|
|
|
|
if (this.muteButton_) {
|
|
this.muteButton_.addEventListener(
|
|
'click', this.onMuteButtonClick_.bind(this));
|
|
}
|
|
|
|
if (this.volumeBar_) {
|
|
this.volumeBar_.addEventListener(
|
|
'input', this.onVolumeInput_.bind(this));
|
|
}
|
|
|
|
this.video_.addEventListener(
|
|
'volumechange', this.onVolumeStateChange_.bind(this));
|
|
// Initialize volume display with a fake event.
|
|
this.onVolumeStateChange_();
|
|
|
|
if (this.captionButton_) {
|
|
this.captionButton_.addEventListener(
|
|
'click', this.onCaptionClick_.bind(this));
|
|
}
|
|
|
|
this.player_.addEventListener(
|
|
'texttrackvisibility', this.onCaptionStateChange_.bind(this));
|
|
this.player_.addEventListener(
|
|
'trackschanged', this.onTracksChange_.bind(this));
|
|
this.player_.addEventListener(
|
|
'variantchanged', this.onVariantChange_.bind(this));
|
|
this.player_.addEventListener(
|
|
'textchanged', this.updateTextLanguages_.bind(this));
|
|
|
|
if (this.fullscreenButton_) {
|
|
this.fullscreenButton_.addEventListener(
|
|
'click', this.onFullscreenClick_.bind(this));
|
|
document.addEventListener(
|
|
'fullscreenchange', this.onFullscreenChange_.bind(this));
|
|
}
|
|
|
|
if (this.currentTime_) {
|
|
this.currentTime_.addEventListener(
|
|
'click', this.onCurrentTimeClick_.bind(this));
|
|
}
|
|
|
|
if (this.rewindButton_) {
|
|
this.rewindButton_.addEventListener(
|
|
'click', this.onRewindClick_.bind(this));
|
|
}
|
|
|
|
if (this.fastForwardButton_) {
|
|
this.fastForwardButton_.addEventListener(
|
|
'click', this.onFastForwardClick_.bind(this));
|
|
}
|
|
|
|
if (this.castButton_) {
|
|
this.castButton_.addEventListener(
|
|
'click', this.onCastClick_.bind(this));
|
|
}
|
|
|
|
if (this.pipButton_) {
|
|
this.pipButton_.addEventListener(
|
|
'click', () => {
|
|
this.onPipClick_();
|
|
});
|
|
this.localVideo_.addEventListener(
|
|
'enterpictureinpicture', this.onEnterPictureInPicture_.bind(this));
|
|
this.localVideo_.addEventListener(
|
|
'leavepictureinpicture', this.onLeavePictureInPicture_.bind(this));
|
|
}
|
|
|
|
this.controlsContainer_.addEventListener(
|
|
'touchstart', this.onContainerTouch_.bind(this), {passive: false});
|
|
this.controlsContainer_.addEventListener(
|
|
'click', this.onContainerClick_.bind(this));
|
|
|
|
this.overflowMenu_.addEventListener(
|
|
'touchstart', function(event) {
|
|
this.lastTouchEventTime_ = Date.now();
|
|
event.stopPropagation();
|
|
}.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;
|
|
});
|
|
}
|
|
|
|
if (this.overflowMenuButton_) {
|
|
this.overflowMenuButton_.addEventListener(
|
|
'click', this.onOverflowMenuButtonClick_.bind(this));
|
|
}
|
|
|
|
if (this.resolutionButton_) {
|
|
this.resolutionButton_.addEventListener(
|
|
'click', this.onResolutionClick_.bind(this));
|
|
}
|
|
|
|
if (this.languagesButton_) {
|
|
this.languagesButton_.addEventListener(
|
|
'click', this.onLanguagesClick_.bind(this));
|
|
}
|
|
|
|
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(
|
|
'mouseout', this.onMouseOut_.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 explicitely hide the menus onclick here.
|
|
this.controlsButtonPanel_.addEventListener('click', () => {
|
|
if (this.anySettingsMenusAreOpen_()) {
|
|
this.hideSettingsMenus_();
|
|
}
|
|
});
|
|
|
|
this.castProxy_.addEventListener(
|
|
'caststatuschanged', this.onCastStatusChange_.bind(this));
|
|
|
|
this.videoContainer_.addEventListener('keyup', this.onKeyUp_.bind(this));
|
|
|
|
for (let i = 0; i < this.backToOverflowMenuButtons_.length; i++) {
|
|
let button = this.backToOverflowMenuButtons_[i];
|
|
button.addEventListener('click', () => {
|
|
// Hide the submenus, display the overflow menu
|
|
this.hideSettingsMenus_();
|
|
shaka.ui.Controls.setDisplay_(this.overflowMenu_, true);
|
|
|
|
// If there are back to overflow menu buttons, there must be
|
|
// overflow menu buttons, but oh well
|
|
if (this.overflowMenu_.childNodes.length) {
|
|
/** @type {!HTMLElement} */ (this.overflowMenu_.childNodes[0]).focus();
|
|
}
|
|
|
|
// Make sure controls are displayed
|
|
this.overrideCssShowControls_ = true;
|
|
});
|
|
}
|
|
|
|
if (screen.orientation) {
|
|
screen.orientation.addEventListener(
|
|
'change', this.onScreenRotation_.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.ui.Controls.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_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addOverflowMenu_ = function() {
|
|
/** @private {!HTMLElement} */
|
|
this.overflowMenu_ = shaka.ui.Controls.createHTMLElement_('div');
|
|
this.overflowMenu_.classList.add('shaka-overflow-menu');
|
|
this.overflowMenu_.classList.add('shaka-no-propagation');
|
|
this.overflowMenu_.classList.add('shaka-show-controls-on-mouse-over');
|
|
this.overflowMenu_.classList.add('shaka-settings-menu');
|
|
this.controlsContainer_.appendChild(this.overflowMenu_);
|
|
|
|
for (let i = 0; i < this.config_.overflowMenuButtons.length; i++) {
|
|
const name = this.config_.overflowMenuButtons[i];
|
|
if (this.elementNamesToFunctions_.get(name)) {
|
|
if (shaka.ui.Controls.overflowButtons_.indexOf(name) == -1) {
|
|
// Not an overflow button, skip
|
|
shaka.log.warning('Element is not part of overflow ' +
|
|
'button collection and will be skipped', name);
|
|
continue;
|
|
}
|
|
this.elementNamesToFunctions_.get(name)();
|
|
}
|
|
}
|
|
|
|
// Add settings menus
|
|
if (this.config_.overflowMenuButtons.indexOf('quality') > -1) {
|
|
this.addResolutionMenu_();
|
|
}
|
|
|
|
if (this.config_.overflowMenuButtons.indexOf('language') > -1) {
|
|
this.addAudioLangMenu_();
|
|
}
|
|
|
|
if (this.config_.overflowMenuButtons.indexOf('captions') > -1) {
|
|
this.addTextLangMenu_();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addCurrentTime_ = function() {
|
|
const timeContainer = shaka.ui.Controls.createHTMLElement_('div');
|
|
timeContainer.classList.add('shaka-time-container');
|
|
this.currentTime_ = shaka.ui.Controls.createHTMLElement_('div');
|
|
this.currentTime_.textContent = '0:00';
|
|
timeContainer.appendChild(this.currentTime_);
|
|
this.controlsButtonPanel_.appendChild(timeContainer);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addMuteButton_ = function() {
|
|
this.muteButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
this.muteButton_.classList.add('shaka-mute-button');
|
|
this.muteButton_.classList.add('material-icons');
|
|
this.muteButton_.textContent = shaka.ui.Controls.MaterialDesignIcons_.MUTE;
|
|
this.controlsButtonPanel_.appendChild(this.muteButton_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addVolumeBar_ = 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.volumeBarContainer_ = shaka.ui.Controls.createHTMLElement_('div');
|
|
this.volumeBarContainer_.classList.add('shaka-volume-bar-container');
|
|
|
|
this.volumeBar_ =
|
|
/** @type {!HTMLInputElement} */ (document.createElement('input'));
|
|
this.volumeBar_.classList.add('shaka-volume-bar');
|
|
this.volumeBar_.setAttribute('type', 'range');
|
|
// NOTE: step=any causes keyboard nav problems on IE 11.
|
|
this.volumeBar_.setAttribute('step', 'any');
|
|
this.volumeBar_.setAttribute('min', '0');
|
|
this.volumeBar_.setAttribute('max', '1');
|
|
this.volumeBar_.setAttribute('value', '0');
|
|
|
|
this.volumeBarContainer_.appendChild(this.volumeBar_);
|
|
this.controlsButtonPanel_.appendChild(this.volumeBarContainer_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addFullscreenButton_ = function() {
|
|
this.fullscreenButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
this.fullscreenButton_.classList.add('shaka-fullscreen-button');
|
|
this.fullscreenButton_.classList.add('material-icons');
|
|
this.fullscreenButton_.textContent =
|
|
shaka.ui.Controls.MaterialDesignIcons_.FULLSCREEN;
|
|
this.controlsButtonPanel_.appendChild(this.fullscreenButton_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addOverflowMenuButton_ = function() {
|
|
this.overflowMenuButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
this.overflowMenuButton_.classList.add('shaka-overflow-menu-button');
|
|
this.overflowMenuButton_.classList.add('shaka-no-propagation');
|
|
this.overflowMenuButton_.classList.add('material-icons');
|
|
this.overflowMenuButton_.textContent =
|
|
shaka.ui.Controls.MaterialDesignIcons_.OPEN_OVERFLOW;
|
|
this.controlsButtonPanel_.appendChild(this.overflowMenuButton_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addCaptionButton_ = function() {
|
|
this.captionButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
this.captionButton_.classList.add('shaka-caption-button');
|
|
this.captionIcon_ = shaka.ui.Controls.createHTMLElement_('i');
|
|
this.captionIcon_.classList.add('material-icons');
|
|
this.captionIcon_.textContent =
|
|
shaka.ui.Controls.MaterialDesignIcons_.CLOSED_CAPTIONS;
|
|
this.captionButton_.appendChild(this.captionIcon_);
|
|
|
|
const label = shaka.ui.Controls.createHTMLElement_('label');
|
|
label.classList.add('shaka-overflow-button-label');
|
|
|
|
this.captionsNameSpan_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
|
|
label.appendChild(this.captionsNameSpan_);
|
|
|
|
this.currentCaptions_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.currentCaptions_.classList.add('shaka-current-selection-span');
|
|
label.appendChild(this.currentCaptions_);
|
|
this.captionButton_.appendChild(label);
|
|
this.overflowMenu_.appendChild(this.captionButton_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addTextLangMenu_ = function() {
|
|
this.textLangMenu_ = shaka.ui.Controls.createHTMLElement_('div');
|
|
this.textLangMenu_.classList.add('shaka-text-languages');
|
|
this.textLangMenu_.classList.add('shaka-no-propagation');
|
|
this.textLangMenu_.classList.add('shaka-show-controls-on-mouse-over');
|
|
this.textLangMenu_.classList.add('shaka-settings-menu');
|
|
|
|
this.backFromCaptionsButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
this.backFromCaptionsButton_.classList.add('shaka-back-to-overflow-button');
|
|
this.textLangMenu_.appendChild(this.backFromCaptionsButton_);
|
|
|
|
const backIcon = shaka.ui.Controls.createHTMLElement_('i');
|
|
backIcon.classList.add('material-icons');
|
|
backIcon.textContent = shaka.ui.Controls.MaterialDesignIcons_.BACK;
|
|
this.backFromCaptionsButton_.appendChild(backIcon);
|
|
|
|
this.backFromCaptionsSpan_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.backFromCaptionsButton_.appendChild(this.backFromCaptionsSpan_);
|
|
|
|
// Add the off option
|
|
const off = shaka.ui.Controls.createHTMLElement_('button');
|
|
off.setAttribute('aria-selected', 'true');
|
|
this.textLangMenu_.appendChild(off);
|
|
|
|
const chosenIcon = shaka.ui.Controls.createHTMLElement_('i');
|
|
chosenIcon.classList.add('material-icons');
|
|
chosenIcon.classList.add('shaka-chosen-item');
|
|
// This text content is actually a material design icon.
|
|
// DO NOT LOCALIZE
|
|
chosenIcon.textContent = shaka.ui.Controls.MaterialDesignIcons_.CHECKMARK;
|
|
// Screen reader should ignore 'done'.
|
|
chosenIcon.setAttribute('aria-hidden', 'true');
|
|
off.appendChild(chosenIcon);
|
|
|
|
this.captionsOffSpan_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
|
|
this.captionsOffSpan_.classList.add('shaka-auto-span');
|
|
off.appendChild(this.captionsOffSpan_);
|
|
|
|
this.controlsContainer_.appendChild(this.textLangMenu_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addCastButton_ = function() {
|
|
this.castButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
|
|
this.castButton_.classList.add('shaka-cast-button');
|
|
this.castButton_.classList.add('shaka-hidden');
|
|
this.castButton_.setAttribute('aria-pressed', 'false');
|
|
|
|
this.castIcon_ = shaka.ui.Controls.createHTMLElement_('i');
|
|
this.castIcon_.classList.add('material-icons');
|
|
// This text content is actually a material design icon.
|
|
// DO NOT LOCALIZE
|
|
this.castIcon_.textContent = shaka.ui.Controls.MaterialDesignIcons_.CAST;
|
|
this.castButton_.appendChild(this.castIcon_);
|
|
|
|
const label = shaka.ui.Controls.createHTMLElement_('label');
|
|
label.classList.add('shaka-overflow-button-label');
|
|
this.castNameSpan_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
label.appendChild(this.castNameSpan_);
|
|
|
|
this.castCurrentSelectionSpan_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.castCurrentSelectionSpan_.classList.add('shaka-current-selection-span');
|
|
label.appendChild(this.castCurrentSelectionSpan_);
|
|
this.castButton_.appendChild(label);
|
|
this.overflowMenu_.appendChild(this.castButton_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addRewindButton_ = function() {
|
|
this.rewindButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
this.rewindButton_.classList.add('shaka-rewind-button');
|
|
this.rewindButton_.classList.add('material-icons');
|
|
this.rewindButton_.textContent =
|
|
shaka.ui.Controls.MaterialDesignIcons_.REWIND;
|
|
this.controlsButtonPanel_.appendChild(this.rewindButton_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addFastForwardButton_ = function() {
|
|
this.fastForwardButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
this.fastForwardButton_.classList.add('shaka-fast-forward-button');
|
|
this.fastForwardButton_.classList.add('material-icons');
|
|
this.fastForwardButton_.textContent =
|
|
shaka.ui.Controls.MaterialDesignIcons_.FAST_FORWARD;
|
|
this.controlsButtonPanel_.appendChild(this.fastForwardButton_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addResolutionMenu_ = function() {
|
|
this.resolutionMenu_ = shaka.ui.Controls.createHTMLElement_('div');
|
|
this.resolutionMenu_.classList.add('shaka-resolutions');
|
|
this.resolutionMenu_.classList.add('shaka-no-propagation');
|
|
this.resolutionMenu_.classList.add('shaka-show-controls-on-mouse-over');
|
|
this.resolutionMenu_.classList.add('shaka-settings-menu');
|
|
|
|
this.backFromResolutionButton_ =
|
|
shaka.ui.Controls.createHTMLElement_('button');
|
|
this.backFromResolutionButton_.classList.add('shaka-back-to-overflow-button');
|
|
this.resolutionMenu_.appendChild(this.backFromResolutionButton_);
|
|
|
|
const backIcon = shaka.ui.Controls.createHTMLElement_('i');
|
|
backIcon.classList.add('material-icons');
|
|
backIcon.textContent = shaka.ui.Controls.MaterialDesignIcons_.BACK;
|
|
this.backFromResolutionButton_.appendChild(backIcon);
|
|
|
|
this.backFromResolutionSpan_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.backFromResolutionButton_.appendChild(this.backFromResolutionSpan_);
|
|
|
|
|
|
// Add the abr option
|
|
const auto = shaka.ui.Controls.createHTMLElement_('button');
|
|
auto.setAttribute('aria-selected', 'true');
|
|
this.resolutionMenu_.appendChild(auto);
|
|
|
|
const chosenIcon = shaka.ui.Controls.createHTMLElement_('i');
|
|
chosenIcon.classList.add('material-icons');
|
|
chosenIcon.classList.add('shaka-chosen-item');
|
|
chosenIcon.textContent = shaka.ui.Controls.MaterialDesignIcons_.CHECKMARK;
|
|
// Screen reader should ignore the checkmark.
|
|
chosenIcon.setAttribute('aria-hidden', 'true');
|
|
auto.appendChild(chosenIcon);
|
|
|
|
this.abrOnSpan_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.abrOnSpan_.classList.add('shaka-auto-span');
|
|
auto.appendChild(this.abrOnSpan_);
|
|
|
|
this.controlsContainer_.appendChild(this.resolutionMenu_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addResolutionButton_ = function() {
|
|
this.resolutionButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
|
|
this.resolutionButton_.classList.add('shaka-resolution-button');
|
|
|
|
const icon = shaka.ui.Controls.createHTMLElement_('i');
|
|
icon.classList.add('material-icons');
|
|
icon.textContent = shaka.ui.Controls.MaterialDesignIcons_.RESOLUTION;
|
|
this.resolutionButton_.appendChild(icon);
|
|
|
|
const label = shaka.ui.Controls.createHTMLElement_('label');
|
|
label.classList.add('shaka-overflow-button-label');
|
|
this.resolutionNameSpan_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
label.appendChild(this.resolutionNameSpan_);
|
|
|
|
this.currentResolution_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.currentResolution_.classList.add('shaka-current-selection-span');
|
|
label.appendChild(this.currentResolution_);
|
|
this.resolutionButton_.appendChild(label);
|
|
|
|
this.overflowMenu_.appendChild(this.resolutionButton_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addAudioLangMenu_ = function() {
|
|
this.audioLangMenu_ = shaka.ui.Controls.createHTMLElement_('div');
|
|
this.audioLangMenu_.classList.add('shaka-audio-languages');
|
|
this.audioLangMenu_.classList.add('shaka-no-propagation');
|
|
this.audioLangMenu_.classList.add('shaka-show-controls-on-mouse-over');
|
|
this.audioLangMenu_.classList.add('shaka-settings-menu');
|
|
|
|
this.backFromLanguageButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
this.backFromLanguageButton_.classList.add('shaka-back-to-overflow-button');
|
|
this.audioLangMenu_.appendChild(this.backFromLanguageButton_);
|
|
|
|
const backIcon = shaka.ui.Controls.createHTMLElement_('i');
|
|
backIcon.classList.add('material-icons');
|
|
backIcon.textContent = shaka.ui.Controls.MaterialDesignIcons_.BACK;
|
|
this.backFromLanguageButton_.appendChild(backIcon);
|
|
|
|
this.backFromLanguageSpan_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.backFromLanguageButton_.appendChild(this.backFromLanguageSpan_);
|
|
|
|
this.controlsContainer_.appendChild(this.audioLangMenu_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addLanguagesButton_ = function() {
|
|
this.languagesButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
this.languagesButton_.classList.add('shaka-language-button');
|
|
|
|
const icon = shaka.ui.Controls.createHTMLElement_('i');
|
|
icon.classList.add('material-icons');
|
|
icon.textContent = shaka.ui.Controls.MaterialDesignIcons_.LANGUAGE;
|
|
this.languagesButton_.appendChild(icon);
|
|
|
|
const label = shaka.ui.Controls.createHTMLElement_('label');
|
|
label.classList.add('shaka-overflow-button-label');
|
|
this.languageNameSpan_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.languageNameSpan_.classList.add('languageSpan');
|
|
label.appendChild(this.languageNameSpan_);
|
|
|
|
this.currentAudioLanguage_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.currentAudioLanguage_.classList.add('shaka-current-selection-span');
|
|
const language = this.player_.getConfiguration().preferredAudioLanguage;
|
|
this.setChosenLanguageName_(this.currentAudioLanguage_, language);
|
|
label.appendChild(this.currentAudioLanguage_);
|
|
|
|
this.languagesButton_.appendChild(label);
|
|
|
|
this.overflowMenu_.appendChild(this.languagesButton_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.addPipButton_ = function() {
|
|
const LocIds = shaka.ui.Locales.Ids;
|
|
this.pipButton_ = shaka.ui.Controls.createHTMLElement_('button');
|
|
|
|
this.pipIcon_ = shaka.ui.Controls.createHTMLElement_('i');
|
|
this.pipIcon_.classList.add('material-icons');
|
|
// This text content is actually a material design icon.
|
|
// DO NOT LOCALIZE
|
|
this.pipIcon_.textContent = shaka.ui.Controls.MaterialDesignIcons_.PIP;
|
|
this.pipButton_.appendChild(this.pipIcon_);
|
|
|
|
const label = shaka.ui.Controls.createHTMLElement_('label');
|
|
label.classList.add('shaka-overflow-button-label');
|
|
this.pipNameSpan_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.pipNameSpan_.textContent =
|
|
this.localization_.resolve(LocIds.LABEL_PICTURE_IN_PICTURE);
|
|
label.appendChild(this.pipNameSpan_);
|
|
|
|
this.currentPipState_ = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.currentPipState_.classList.add('shaka-current-selection-span');
|
|
this.currentPipState_.textContent =
|
|
this.localization_.resolve(LocIds.LABEL_PICTURE_IN_PICTURE_OFF);
|
|
label.appendChild(this.currentPipState_);
|
|
|
|
this.pipButton_.appendChild(label);
|
|
|
|
this.overflowMenu_.appendChild(this.pipButton_);
|
|
|
|
// Don't display the button if PiP is not supported or not allowed
|
|
// TODO: Can this ever change? Is it worth creating the button if the below
|
|
// condition is true?
|
|
if (!this.isPipAllowed_()) {
|
|
shaka.ui.Controls.setDisplay_(this.pipButton_, false);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {boolean}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.isPipAllowed_ = function() {
|
|
return document.pictureInPictureEnabled &&
|
|
!this.video_.disablePictureInPicture;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
* @param {string} language
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.setChosenLanguageName_ =
|
|
function(element, language) {
|
|
if (language.length) {
|
|
let languageName;
|
|
if (mozilla.LanguageMapping[language]) {
|
|
languageName = mozilla.LanguageMapping[language].nativeName;
|
|
} else {
|
|
// We don't know this language
|
|
languageName = this.localization_.resolve(
|
|
shaka.ui.Controls.resolveSpecialLanguageCode_(language));
|
|
}
|
|
element.textContent = languageName;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* 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.Controls.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.Controls.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_;
|
|
};
|
|
|
|
|
|
/**
|
|
* When a mobile device is rotated to landscape layout, and the video is loaded,
|
|
* make the demo app go into fullscreen.
|
|
* Similarly, exit fullscreen when the device is rotated to portrait layout.
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onScreenRotation_ = function() {
|
|
if (!this.video_ ||
|
|
this.video_.readyState == 0 ||
|
|
this.castProxy_.isCasting()) return;
|
|
|
|
if (screen.orientation.type.includes('landscape') &&
|
|
!document.fullscreenElement) {
|
|
this.videoContainer_.requestFullscreen();
|
|
} else if (screen.orientation.type.includes('portrait') &&
|
|
document.fullscreenElement) {
|
|
document.exitFullscreen();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* 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) {
|
|
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 = '';
|
|
// Show the controls.
|
|
this.setControlsOpacity_(shaka.ui.Controls.Opacity_.OPAQUE);
|
|
this.hideSettingsMenusTimer_.stop();
|
|
this.updateTimeAndSeekRange_();
|
|
|
|
// Hide the cursor when the mouse stops moving.
|
|
// Only applies while the cursor is over the video container.
|
|
if (this.mouseStillTimeoutId_) {
|
|
// Reset the timer.
|
|
window.clearTimeout(this.mouseStillTimeoutId_);
|
|
}
|
|
|
|
// Only start a timeout on 'touchend' or for 'mousemove' with no touch events.
|
|
if (event.type == 'touchend' ||
|
|
event.type == 'keyup'|| !this.lastTouchEventTime_) {
|
|
this.mouseStillTimeoutId_ = window.setTimeout(
|
|
this.onMouseStill_.bind(this), 3000);
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onMouseOut_ = function() {
|
|
// We sometimes get 'mouseout' events with touches. Since we can never leave
|
|
// the video element when touching, ignore.
|
|
if (this.lastTouchEventTime_) return;
|
|
|
|
// Expire the timer early.
|
|
if (this.mouseStillTimeoutId_) {
|
|
window.clearTimeout(this.mouseStillTimeoutId_);
|
|
}
|
|
// Run the timeout callback 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.onMouseStill_();
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onMouseStill_ = function() {
|
|
// The mouse has stopped moving.
|
|
this.mouseStillTimeoutId_ = null;
|
|
// 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.Controls.Opacity_.OPAQUE);
|
|
} else {
|
|
this.setControlsOpacity_(shaka.ui.Controls.Opacity_.TRANSPARENT);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onContainerTouch_ = function(event) {
|
|
if (!this.video_.duration) {
|
|
// Can't play yet. Ignore.
|
|
return;
|
|
}
|
|
|
|
// If the overflow menu is showing, hide it on a touch event
|
|
if (this.overflowMenu_.classList.contains('shaka-displayed')) {
|
|
shaka.ui.Controls.setDisplay_(this.overflowMenu_, false);
|
|
// Stop this event from becoming a click event.
|
|
event.preventDefault();
|
|
}
|
|
|
|
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.hideSettingsMenus_();
|
|
} 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();
|
|
this.trickPlayRate_ = 1;
|
|
|
|
if (this.video_.paused) {
|
|
this.video_.play();
|
|
} else {
|
|
this.video_.pause();
|
|
}
|
|
};
|
|
|
|
|
|
/** @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();
|
|
}
|
|
|
|
const Controls = shaka.ui.Controls;
|
|
// 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(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(shaka.ui.Locales.Ids.ARIA_LABEL_PLAY));
|
|
} else {
|
|
this.playButton_.setAttribute('icon', 'pause');
|
|
this.playButton_.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(shaka.ui.Locales.Ids.ARIA_LABEL_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_();
|
|
|
|
// Collect input events and seek when things have been stable for 125ms.
|
|
if (this.seekTimeoutId_ != null) {
|
|
window.clearTimeout(this.seekTimeoutId_);
|
|
}
|
|
this.seekTimeoutId_ = window.setTimeout(
|
|
this.onSeekInputTimeout_.bind(this), 125);
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onSeekInputTimeout_ = function() {
|
|
goog.asserts.assert(this.seekBar_ != null, 'Seekbar should not be null!');
|
|
this.seekTimeoutId_ = null;
|
|
this.video_.currentTime = parseFloat(this.seekBar_.value);
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onSeekEnd_ = function() {
|
|
if (!this.enabled_) return;
|
|
|
|
if (this.seekTimeoutId_ != null) {
|
|
// They just let go of the seek bar, so end the timer early.
|
|
window.clearTimeout(this.seekTimeoutId_);
|
|
this.onSeekInputTimeout_();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onMuteButtonClick_ = function() {
|
|
if (!this.enabled_) return;
|
|
|
|
this.video_.muted = !this.video_.muted;
|
|
};
|
|
|
|
|
|
/**
|
|
* Updates the controls to reflect volume changes.
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onVolumeStateChange_ = function() {
|
|
const Controls = shaka.ui.Controls;
|
|
if (this.video_.muted) {
|
|
if (this.muteButton_) {
|
|
this.muteButton_.textContent =
|
|
shaka.ui.Controls.MaterialDesignIcons_.UNMUTE;
|
|
this.muteButton_.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(shaka.ui.Locales.Ids.ARIA_LABEL_UNMUTE));
|
|
}
|
|
if (this.volumeBar_) {
|
|
this.volumeBar_.value = 0;
|
|
}
|
|
} else {
|
|
if (this.muteButton_) {
|
|
this.muteButton_.textContent =
|
|
shaka.ui.Controls.MaterialDesignIcons_.MUTE;
|
|
this.muteButton_.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(shaka.ui.Locales.Ids.ARIA_LABEL_MUTE));
|
|
}
|
|
if (this.volumeBar_) {
|
|
this.volumeBar_.value = this.video_.volume;
|
|
}
|
|
}
|
|
|
|
if (this.volumeBar_) {
|
|
let gradient = ['to right'];
|
|
gradient.push(shaka.ui.Controls.VOLUME_BAR_VOLUME_LEVEL_COLOR_ +
|
|
(this.volumeBar_.value * 100) + '%');
|
|
gradient.push(shaka.ui.Controls.VOLUME_BAR_BASE_COLOR_ +
|
|
(this.volumeBar_.value * 100) + '%');
|
|
gradient.push(shaka.ui.Controls.VOLUME_BAR_BASE_COLOR_ + '100%');
|
|
this.volumeBarContainer_.style.background =
|
|
'linear-gradient(' + gradient.join(',') + ')';
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onVolumeInput_ = function() {
|
|
this.video_.volume = parseFloat(this.volumeBar_.value);
|
|
if (this.video_.volume == 0) {
|
|
this.video_.muted = true;
|
|
} else {
|
|
this.video_.muted = false;
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onCaptionClick_ = function() {
|
|
if (!this.enabled_) return;
|
|
|
|
shaka.ui.Controls.setDisplay_(this.overflowMenu_, false);
|
|
shaka.ui.Controls.setDisplay_(this.textLangMenu_, true);
|
|
// Focus on the currently selected language button.
|
|
this.focusOnTheChosenItem_(this.textLangMenu_);
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onResolutionClick_ = function() {
|
|
if (!this.enabled_) return;
|
|
shaka.ui.Controls.setDisplay_(this.overflowMenu_, false);
|
|
shaka.ui.Controls.setDisplay_(this.resolutionMenu_, true);
|
|
// Focus on the currently selected resolution button.
|
|
this.focusOnTheChosenItem_(this.resolutionMenu_);
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onLanguagesClick_ = function() {
|
|
if (!this.enabled_) return;
|
|
shaka.ui.Controls.setDisplay_(this.overflowMenu_, false);
|
|
shaka.ui.Controls.setDisplay_(this.audioLangMenu_, true);
|
|
// Focus on the currently selected language button.
|
|
this.focusOnTheChosenItem_(this.audioLangMenu_);
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onTracksChange_ = function() {
|
|
// TS content might have captions embedded in video stream, we can't know
|
|
// until we start transmuxing. So, always show caption button if we're
|
|
// playing TS content.
|
|
if (this.captionButton_) {
|
|
if (shaka.ui.Utils.isTsContent(this.player_)) {
|
|
shaka.ui.Controls.setDisplay_(this.captionButton_, true);
|
|
} else {
|
|
let hasText = this.player_.getTextTracks().length;
|
|
shaka.ui.Controls.setDisplay_(this.captionButton_, hasText > 0);
|
|
}
|
|
}
|
|
|
|
// Update language and resolution selections
|
|
this.updateResolutionSelection_();
|
|
this.updateAudioLanguages_();
|
|
this.updateTextLanguages_();
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onVariantChange_ = function() {
|
|
// Update language and resolution selections
|
|
this.updateResolutionSelection_();
|
|
this.updateAudioLanguages_();
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.updateResolutionSelection_ = function() {
|
|
// Only applicable if resolution button is a part of the UI
|
|
if (!this.resolutionButton_ || !this.resolutionMenu_) {
|
|
return;
|
|
}
|
|
|
|
let tracks = this.player_.getVariantTracks();
|
|
// Hide resolution menu and button for audio-only content.
|
|
if (tracks.length && !tracks[0].height) {
|
|
shaka.ui.Controls.setDisplay_(this.resolutionMenu_, false);
|
|
shaka.ui.Controls.setDisplay_(this.resolutionButton_, false);
|
|
return;
|
|
}
|
|
tracks.sort(function(t1, t2) {
|
|
return t1.height - t2.height;
|
|
});
|
|
tracks.reverse();
|
|
|
|
// If there is a selected variant track, then we filtering out any tracks in
|
|
// a different language. Then we use those remaining tracks to display the
|
|
// available resolutions.
|
|
const selectedTrack = tracks.find((track) => track.active);
|
|
if (selectedTrack) {
|
|
const language = selectedTrack.language;
|
|
// Filter by current audio language.
|
|
tracks = tracks.filter(function(track) {
|
|
return track.language == language;
|
|
});
|
|
}
|
|
|
|
// Remove old shaka-resolutions
|
|
// 1. Save the back to menu button
|
|
const backButton = shaka.ui.Utils.getFirstDescendantWithClassName(
|
|
this.resolutionMenu_, 'shaka-back-to-overflow-button');
|
|
|
|
// 2. Remove everything
|
|
while (this.resolutionMenu_.firstChild) {
|
|
this.resolutionMenu_.removeChild(this.resolutionMenu_.firstChild);
|
|
}
|
|
|
|
// 3. Add the backTo Menu button back
|
|
this.resolutionMenu_.appendChild(backButton);
|
|
|
|
const abrEnabled = this.player_.getConfiguration().abr.enabled;
|
|
|
|
// Add new ones
|
|
tracks.forEach((track) => {
|
|
let button = shaka.ui.Controls.createHTMLElement_('button');
|
|
button.classList.add('explicit-resolution');
|
|
button.addEventListener('click',
|
|
this.onTrackSelected_.bind(this, track));
|
|
|
|
let span = shaka.ui.Controls.createHTMLElement_('span');
|
|
span.textContent = track.height + 'p';
|
|
button.appendChild(span);
|
|
|
|
if (!abrEnabled && track == selectedTrack) {
|
|
// If abr is disabled, mark the selected track's
|
|
// resolution.
|
|
button.setAttribute('aria-selected', 'true');
|
|
button.appendChild(this.chosenIcon_());
|
|
span.classList.add('shaka-chosen-item');
|
|
this.currentResolution_.textContent = span.textContent;
|
|
}
|
|
this.resolutionMenu_.appendChild(button);
|
|
});
|
|
|
|
// Add the Auto button
|
|
let autoButton = shaka.ui.Controls.createHTMLElement_('button');
|
|
autoButton.addEventListener('click', function() {
|
|
let config = {abr: {enabled: true}};
|
|
this.player_.configure(config);
|
|
this.updateResolutionSelection_();
|
|
}.bind(this));
|
|
|
|
let autoSpan = shaka.ui.Controls.createHTMLElement_('span');
|
|
autoSpan.textContent =
|
|
this.localization_.resolve(shaka.ui.Locales.Ids.LABEL_AUTO_QUALITY);
|
|
autoButton.appendChild(autoSpan);
|
|
|
|
// If abr is enabled reflect it by marking 'Auto'
|
|
// as selected.
|
|
if (abrEnabled) {
|
|
autoButton.setAttribute('aria-selected', 'true');
|
|
autoButton.appendChild(this.chosenIcon_());
|
|
|
|
autoSpan.classList.add('shaka-chosen-item');
|
|
|
|
this.currentResolution_.textContent =
|
|
this.localization_.resolve(shaka.ui.Locales.Ids.LABEL_AUTO_QUALITY);
|
|
}
|
|
|
|
this.resolutionMenu_.appendChild(autoButton);
|
|
this.focusOnTheChosenItem_(this.resolutionMenu_);
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.updateAudioLanguages_ = function() {
|
|
// Only applicable if language button is a part of the UI
|
|
if (!this.languagesButton_ ||
|
|
!this.audioLangMenu_ || !this.currentAudioLanguage_) {
|
|
return;
|
|
}
|
|
|
|
const tracks = this.player_.getVariantTracks();
|
|
|
|
const languagesAndRoles = this.player_.getAudioLanguagesAndRoles();
|
|
const languages = languagesAndRoles.map((langAndRole) => {
|
|
return langAndRole.language;
|
|
});
|
|
|
|
this.updateLanguages_(tracks, this.audioLangMenu_, languages,
|
|
this.onAudioLanguageSelected_, /* updateChosen */ true,
|
|
this.currentAudioLanguage_);
|
|
this.focusOnTheChosenItem_(this.audioLangMenu_);
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.updateTextLanguages_ = function() {
|
|
// Only applicable if captions button is a part of the UI
|
|
if (!this.captionButton_ || !this.textLangMenu_ ||
|
|
!this.currentCaptions_) {
|
|
return;
|
|
}
|
|
|
|
const tracks = this.player_.getTextTracks();
|
|
|
|
const languagesAndRoles = this.player_.getTextLanguagesAndRoles();
|
|
const languages = languagesAndRoles.map((langAndRole) => {
|
|
return langAndRole.language;
|
|
});
|
|
|
|
this.updateLanguages_(tracks, this.textLangMenu_, languages,
|
|
this.onTextLanguageSelected_,
|
|
/* Don't mark current text language as chosen unless captions are enabled */
|
|
this.player_.isTextTrackVisible(),
|
|
this.currentCaptions_);
|
|
|
|
// Add the Off button
|
|
let offButton = shaka.ui.Controls.createHTMLElement_('button');
|
|
offButton.addEventListener('click', () => {
|
|
this.player_.setTextTrackVisibility(false);
|
|
this.updateTextLanguages_();
|
|
});
|
|
|
|
offButton.appendChild(this.captionsOffSpan_);
|
|
|
|
this.textLangMenu_.appendChild(offButton);
|
|
|
|
if (!this.player_.isTextTrackVisible()) {
|
|
offButton.setAttribute('aria-selected', 'true');
|
|
offButton.appendChild(this.chosenIcon_());
|
|
this.captionsOffSpan_.classList.add('shaka-chosen-item');
|
|
this.currentCaptions_.textContent =
|
|
this.localization_.resolve(shaka.ui.Locales.Ids.LABEL_CAPTIONS_OFF);
|
|
}
|
|
|
|
this.focusOnTheChosenItem_(this.textLangMenu_);
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!Array.<shaka.extern.Track>} tracks
|
|
* @param {!HTMLElement} langMenu
|
|
* @param {!Array.<string>} languages
|
|
* @param {function(string)} onLanguageSelected
|
|
* @param {boolean} updateChosen
|
|
* @param {!HTMLElement} currentSelectionElement
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.updateLanguages_ = function(tracks, langMenu,
|
|
languages, onLanguageSelected, updateChosen, currentSelectionElement) {
|
|
// Using array.filter(f)[0] as an alternative to array.find(f) which is
|
|
// not supported in IE11.
|
|
const activeTracks = tracks.filter(function(track) {
|
|
return track.active == true;
|
|
});
|
|
const selectedTrack = activeTracks[0];
|
|
|
|
// Remove old languages
|
|
// 1. Save the back to menu button
|
|
const backButton = shaka.ui.Utils.getFirstDescendantWithClassName(
|
|
langMenu, 'shaka-back-to-overflow-button');
|
|
|
|
// 2. Remove everything
|
|
while (langMenu.firstChild) {
|
|
langMenu.removeChild(langMenu.firstChild);
|
|
}
|
|
|
|
// 3. Add the backTo Menu button back
|
|
langMenu.appendChild(backButton);
|
|
|
|
// 4. Add new buttons
|
|
languages.forEach((language) => {
|
|
let button = shaka.ui.Controls.createHTMLElement_('button');
|
|
button.addEventListener('click', onLanguageSelected.bind(this, language));
|
|
|
|
let span = shaka.ui.Controls.createHTMLElement_('span');
|
|
this.setChosenLanguageName_(span, language);
|
|
button.appendChild(span);
|
|
|
|
if (updateChosen && (language == selectedTrack.language)) {
|
|
button.appendChild(this.chosenIcon_());
|
|
span.classList.add('shaka-chosen-item');
|
|
button.setAttribute('aria-selected', 'true');
|
|
currentSelectionElement.textContent = span.textContent;
|
|
}
|
|
langMenu.appendChild(button);
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!shaka.extern.Track} track
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onTrackSelected_ = function(track) {
|
|
// Disable abr manager before changing tracks.
|
|
let config = {abr: {enabled: false}};
|
|
this.player_.configure(config);
|
|
|
|
this.player_.selectVariantTrack(track, /* clearBuffer */ true);
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {string} language
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onAudioLanguageSelected_ = function(language) {
|
|
this.player_.selectAudioLanguage(language);
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {string} language
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onTextLanguageSelected_ = async function(language) {
|
|
await this.player_.setTextTrackVisibility(true);
|
|
this.player_.selectTextLanguage(language);
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {HTMLElement} menu
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.focusOnTheChosenItem_ = function(menu) {
|
|
if (!menu) return;
|
|
const chosenItem = shaka.ui.Utils.getDescendantIfExists(
|
|
menu, 'shaka-chosen-item');
|
|
if (chosenItem) {
|
|
chosenItem.parentElement.focus();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {!Element}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.chosenIcon_ = function() {
|
|
let chosenIcon = shaka.ui.Controls.createHTMLElement_('i');
|
|
chosenIcon.classList.add('material-icons');
|
|
chosenIcon.textContent = shaka.ui.Controls.MaterialDesignIcons_.CHECKMARK;
|
|
// Screen reader should ignore 'done'.
|
|
chosenIcon.setAttribute('aria-hidden', 'true');
|
|
return chosenIcon;
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onCaptionStateChange_ = function() {
|
|
if (this.captionIcon_) {
|
|
if (this.player_.isTextTrackVisible()) {
|
|
this.captionIcon_.classList.add('shaka-captions-on');
|
|
this.captionIcon_.classList.remove('shaka-captions-off');
|
|
} else {
|
|
this.captionIcon_.classList.add('shaka-captions-off');
|
|
this.captionIcon_.classList.remove('shaka-captions-on');
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onFullscreenClick_ = async function() {
|
|
if (!this.enabled_) return;
|
|
|
|
// This is called when the button is clicked. Whichever state we're in, go
|
|
// to the other one.
|
|
if (document.fullscreenElement) {
|
|
document.exitFullscreen();
|
|
} else {
|
|
await this.videoContainer_.requestFullscreen();
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onFullscreenChange_ = function() {
|
|
if (!this.enabled_) return;
|
|
|
|
// This is called after the change has taken effect. It could be initiated
|
|
// by a click on the fullscreen button, but it could also be initiated by the
|
|
// user agent in some other way.
|
|
|
|
const Controls = shaka.ui.Controls;
|
|
const LocIds = shaka.ui.Locales.Ids;
|
|
if (document.fullscreenElement) {
|
|
// We are in fullscreen mode, so offer a button to leave it.
|
|
this.fullscreenButton_.textContent =
|
|
shaka.ui.Controls.MaterialDesignIcons_.EXIT_FULLSCREEN;
|
|
this.fullscreenButton_.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(LocIds.ARIA_LABEL_EXIT_FULL_SCREEN));
|
|
} else {
|
|
// We are not in fullscreen mode, so offer a button to enter it.
|
|
this.fullscreenButton_.textContent =
|
|
shaka.ui.Controls.MaterialDesignIcons_.FULLSCREEN;
|
|
this.fullscreenButton_.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(LocIds.ARIA_LABEL_FULL_SCREEN));
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onCurrentTimeClick_ = function() {
|
|
if (!this.enabled_) return;
|
|
|
|
// Jump to LIVE if the user clicks on the current time.
|
|
if (this.player_.isLive()) {
|
|
this.video_.currentTime = this.player_.seekRange().end;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Cycles trick play rate between -1, -2, -4, and -8.
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onRewindClick_ = function() {
|
|
if (!this.enabled_) return;
|
|
|
|
if (!this.video_.duration) {
|
|
return;
|
|
}
|
|
|
|
this.trickPlayRate_ = (this.trickPlayRate_ > 0 || this.trickPlayRate_ < -4) ?
|
|
-1 : this.trickPlayRate_ * 2;
|
|
this.player_.trickPlay(this.trickPlayRate_);
|
|
};
|
|
|
|
|
|
/**
|
|
* Cycles trick play rate between 1, 2, 4, and 8.
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onFastForwardClick_ = function() {
|
|
if (!this.enabled_) return;
|
|
|
|
if (!this.video_.duration) {
|
|
return;
|
|
}
|
|
|
|
this.trickPlayRate_ = (this.trickPlayRate_ < 0 || this.trickPlayRate_ > 4) ?
|
|
1 : this.trickPlayRate_ * 2;
|
|
this.player_.trickPlay(this.trickPlayRate_);
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onCastClick_ = async function() {
|
|
if (!this.enabled_) return;
|
|
|
|
if (this.castProxy_.isCasting()) {
|
|
this.castProxy_.suggestDisconnect();
|
|
} else {
|
|
this.castButton_.disabled = true;
|
|
this.castProxy_.cast().then(function() {
|
|
this.castButton_.disabled = false;
|
|
// Success!
|
|
}.bind(this), function(error) {
|
|
this.castButton_.disabled = false;
|
|
if (error.code != shaka.util.Error.Code.CAST_CANCELED_BY_USER) {
|
|
this.dispatchEvent(new shaka.util.FakeEvent('error', {
|
|
errorDetails: error,
|
|
}));
|
|
}
|
|
}.bind(this));
|
|
|
|
// If we're in picture-in-picture state, exit
|
|
if (document.pictureInPictureElement && this.pipButton_ != null) {
|
|
await this.onPipClick_();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onPipClick_ = async function() {
|
|
if (!this.enabled_) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (!document.pictureInPictureElement) {
|
|
await this.video_.requestPictureInPicture();
|
|
} else {
|
|
await document.exitPictureInPicture();
|
|
}
|
|
} catch (error) {
|
|
this.dispatchEvent(new shaka.util.FakeEvent('error', {
|
|
errorDetails: error,
|
|
}));
|
|
}
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onEnterPictureInPicture_ = function() {
|
|
if (!this.enabled_) {
|
|
return;
|
|
}
|
|
|
|
const LocIds = shaka.ui.Locales.Ids;
|
|
const Controls = shaka.ui.Controls;
|
|
this.pipIcon_.textContent = Controls.MaterialDesignIcons_.EXIT_PIP;
|
|
this.pipButton_.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(LocIds.ARIA_LABEL_EXIT_PICTURE_IN_PICTURE));
|
|
this.currentPipState_.textContent =
|
|
this.localization_.resolve(LocIds.LABEL_PICTURE_IN_PICTURE_ON);
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onLeavePictureInPicture_ = function() {
|
|
if (!this.enabled_) {
|
|
return;
|
|
}
|
|
|
|
const LocIds = shaka.ui.Locales.Ids;
|
|
const Controls = shaka.ui.Controls;
|
|
this.pipIcon_.textContent = Controls.MaterialDesignIcons_.PIP;
|
|
this.pipButton_.setAttribute(Controls.ARIA_LABEL_,
|
|
this.localization_.resolve(LocIds.ARIA_LABEL_ENTER_PICTURE_IN_PICTURE));
|
|
this.currentPipState_.textContent =
|
|
this.localization_.resolve(LocIds.LABEL_PICTURE_IN_PICTURE_OFF);
|
|
};
|
|
|
|
|
|
/** @private */
|
|
shaka.ui.Controls.prototype.onOverflowMenuButtonClick_ = function() {
|
|
if (this.anySettingsMenusAreOpen_()) {
|
|
this.hideSettingsMenus_();
|
|
} else {
|
|
shaka.ui.Controls.setDisplay_(this.overflowMenu_, true);
|
|
this.overrideCssShowControls_ = true;
|
|
// If overflow menu has currently visible buttons, focus on the
|
|
// first one, when the menu opens.
|
|
const isDisplayed = function(element) {
|
|
return element.classList.contains('shaka-hidden') == false;
|
|
};
|
|
|
|
const Iterables = shaka.util.Iterables;
|
|
if (Iterables.some(this.overflowMenu_.childNodes, isDisplayed)) {
|
|
// Focus on the first visible child of the overflow menu
|
|
const visibleElements =
|
|
Iterables.filter(this.overflowMenu_.childNodes, isDisplayed);
|
|
/** @type {!HTMLElement} */ (visibleElements[0]).focus();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onCastStatusChange_ = function(event) {
|
|
const canCast = this.castProxy_.canCast() && this.castAllowed_;
|
|
const isCasting = this.castProxy_.isCasting();
|
|
this.dispatchEvent(new shaka.util.FakeEvent('caststatuschanged', {
|
|
newStatus: isCasting,
|
|
}));
|
|
|
|
if (this.castButton_) {
|
|
const materialDesignIcons = shaka.ui.Controls.MaterialDesignIcons_;
|
|
shaka.ui.Controls.setDisplay_(this.castButton_, canCast);
|
|
this.castIcon_.textContent = isCasting ?
|
|
materialDesignIcons.EXIT_CAST :
|
|
materialDesignIcons.CAST;
|
|
|
|
// Aria-pressed set to true when casting, set to false otherwise.
|
|
if (canCast) {
|
|
if (isCasting) {
|
|
this.castButton_.setAttribute('aria-pressed', 'true');
|
|
} else {
|
|
this.castButton_.setAttribute('aria-pressed', 'false');
|
|
}
|
|
}
|
|
}
|
|
|
|
this.setCurrentCastSelection_();
|
|
|
|
const pipIsEnabled = (this.isPipAllowed_() && (this.pipButton_ != null));
|
|
if (isCasting) {
|
|
this.controlsContainer_.setAttribute('casting', 'true');
|
|
// Picture-in-picture is not applicable if we're casting
|
|
if (pipIsEnabled) {
|
|
shaka.ui.Controls.setDisplay_(this.pipButton_, false);
|
|
}
|
|
} else {
|
|
this.controlsContainer_.removeAttribute('casting');
|
|
if (pipIsEnabled) {
|
|
shaka.ui.Controls.setDisplay_(this.pipButton_, true);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onBufferingStateChange_ = function(event) {
|
|
// Using [] notation to access buffering property to work around
|
|
// a compiler error.
|
|
const isBuffering = event['buffering'];
|
|
shaka.ui.Controls.setDisplay_(this.bufferingSpinner_, isBuffering);
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.setCurrentCastSelection_ = function() {
|
|
if (!this.castCurrentSelectionSpan_) {
|
|
return;
|
|
}
|
|
|
|
if (this.castProxy_.isCasting()) {
|
|
this.castCurrentSelectionSpan_.textContent = this.castProxy_.receiverName();
|
|
} else {
|
|
this.castCurrentSelectionSpan_.textContent =
|
|
this.localization_.resolve(shaka.ui.Locales.Ids.LABEL_NOT_CASTING);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
let Controls = shaka.ui.Controls;
|
|
let displayTime = this.isSeeking_ ?
|
|
Number(this.seekBar_.value) :
|
|
Number(this.video_.currentTime);
|
|
let duration = this.video_.duration;
|
|
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);
|
|
|
|
let showHour = seekRangeSize >= 3600;
|
|
|
|
// Consider "LIVE" when less than 1 second behind the live-edge. Always
|
|
// show the full time string when seeking, including the leading '-';
|
|
// otherwise, the time string "flickers" near the live-edge.
|
|
if (this.currentTime_) {
|
|
if ((displayTime >= 1) || this.isSeeking_) {
|
|
this.currentTime_.textContent =
|
|
'- ' + this.buildTimeString_(displayTime, showHour);
|
|
this.currentTime_.style.cursor = 'pointer';
|
|
} else {
|
|
this.currentTime_.textContent =
|
|
this.localization_.resolve(shaka.ui.Locales.Ids.LABEL_LIVE);
|
|
this.currentTime_.style.cursor = '';
|
|
}
|
|
}
|
|
|
|
|
|
if (!this.isSeeking_ && this.seekBar_) {
|
|
this.seekBar_.value = seekRange.end - displayTime;
|
|
}
|
|
} else {
|
|
let showHour = duration >= 3600;
|
|
|
|
if (this.currentTime_) {
|
|
this.currentTime_.textContent =
|
|
this.buildTimeString_(displayTime, showHour);
|
|
}
|
|
|
|
|
|
if (duration && this.currentTime_) {
|
|
this.currentTime_.textContent += ' / ' +
|
|
this.buildTimeString_(duration, showHour);
|
|
}
|
|
|
|
if (!this.isSeeking_ && this.seekBar_) {
|
|
this.seekBar_.value = displayTime;
|
|
}
|
|
|
|
if (this.currentTime_) {
|
|
this.currentTime_.style.cursor = '';
|
|
}
|
|
}
|
|
|
|
if (this.seekBar_) {
|
|
// Hide seekbar seek window is very small.
|
|
const seekRange = this.player_.seekRange();
|
|
const seekWindow = seekRange.end - seekRange.start;
|
|
if (seekWindow < shaka.ui.Controls.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR_ ) {
|
|
shaka.ui.Controls.setDisplay_(this.seekBarContainer_, false);
|
|
for (let menu of this.settingsMenus_) {
|
|
menu.classList.add('shaka-low-position');
|
|
}
|
|
} else {
|
|
shaka.ui.Controls.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(Controls.SEEK_BAR_BASE_COLOR_ + ' ' +
|
|
(bufferStartFraction * 100) + '%');
|
|
gradient.push(Controls.SEEK_BAR_PLAYED_COLOR_ + ' ' +
|
|
(bufferStartFraction * 100) + '%');
|
|
gradient.push(Controls.SEEK_BAR_PLAYED_COLOR_ + ' ' +
|
|
(playheadFraction * 100) + '%');
|
|
gradient.push(Controls.SEEK_BAR_BUFFERED_COLOR_ + ' ' +
|
|
(playheadFraction * 100) + '%');
|
|
gradient.push(Controls.SEEK_BAR_BUFFERED_COLOR_ + ' ' +
|
|
(bufferEndFraction * 100) + '%');
|
|
gradient.push(Controls.SEEK_BAR_BASE_COLOR_ + ' ' +
|
|
(bufferEndFraction * 100) + '%');
|
|
}
|
|
this.seekBarContainer_.style.background =
|
|
'linear-gradient(' + gradient.join(',') + ')';
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Builds a time string, e.g., 01:04:23, from |displayTime|.
|
|
*
|
|
* @param {number} displayTime
|
|
* @param {boolean} showHour
|
|
* @return {string}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.buildTimeString_ = function(displayTime, showHour) {
|
|
let h = Math.floor(displayTime / 3600);
|
|
let m = Math.floor((displayTime / 60) % 60);
|
|
let s = Math.floor(displayTime % 60);
|
|
if (s < 10) s = '0' + s;
|
|
let text = m + ':' + s;
|
|
if (showHour) {
|
|
if (m < 10) text = '0' + text;
|
|
text = h + ':' + text;
|
|
}
|
|
return text;
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds class for keyboard navigation if tab was pressed.
|
|
*
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onKeyDown_ = function(event) {
|
|
if (event.keyCode == shaka.ui.Controls.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));
|
|
} else if (event.keyCode == shaka.ui.Controls.KEYCODE_ESCAPE_ &&
|
|
this.anySettingsMenusAreOpen_()) {
|
|
this.hideSettingsMenus_();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Removes class for keyboard navigation if mouse navigation
|
|
* is active.
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.onMouseDown_ = function() {
|
|
// Disable blue outline for focused elements for mouse
|
|
// navigation.
|
|
this.controlsContainer_.classList.remove('shaka-keyboard-navigation');
|
|
|
|
this.eventManager_.unlisten(window, 'mousedown');
|
|
this.eventManager_.listen(window, 'keydown', this.onKeyDown_.bind(this));
|
|
};
|
|
|
|
|
|
/**
|
|
* Depending on the value of display, sets/removes css class of element to
|
|
* either display it or hide.
|
|
*
|
|
* @param {Element} element
|
|
* @param {boolean} display
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.setDisplay_ = function(element, display) {
|
|
if (!element) return;
|
|
if (display) {
|
|
element.classList.add('shaka-displayed');
|
|
// Removing a non-existent class doesn't throw, so, even if
|
|
// the element is not hidden, this should be fine. Same for displayed
|
|
// below.
|
|
element.classList.remove('shaka-hidden');
|
|
} else {
|
|
element.classList.add('shaka-hidden');
|
|
element.classList.remove('shaka-displayed');
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.hideSettingsMenus_ = function() {
|
|
for (let menu of this.settingsMenus_) {
|
|
shaka.ui.Controls.setDisplay_(/** @type {!HTMLElement} */ (menu), false);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @return {boolean}
|
|
*/
|
|
shaka.ui.Controls.prototype.anySettingsMenusAreOpen_ = function() {
|
|
return this.settingsMenus_.some(
|
|
(menu) => menu.classList.contains('shaka-displayed'));
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {!shaka.ui.Controls.Opacity_} opacity
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.prototype.setControlsOpacity_ = function(opacity) {
|
|
if (opacity == shaka.ui.Controls.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 immidiately initiaites another mouse move to
|
|
// interact with the menus. If that didn't happen, go ahead and hide
|
|
// the menus.
|
|
this.hideSettingsMenusTimer_.start(/* seconds= */ 2,
|
|
/* repeating= */ false);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* 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;
|
|
};
|
|
|
|
|
|
/**
|
|
* Resolve a special language code to a name/description enum.
|
|
*
|
|
* @param {string} lang
|
|
* @return {string}
|
|
*/
|
|
shaka.ui.Controls.resolveSpecialLanguageCode_ = function(lang) {
|
|
if (lang == 'mul') {
|
|
return shaka.ui.Locales.Ids.LABEL_MULTIPLE_LANGUAGES;
|
|
} else if (lang == 'zxx') {
|
|
return shaka.ui.Locales.Ids.LABEL_NOT_APPLICABLE;
|
|
} else {
|
|
return shaka.ui.Locales.Ids.LABEL_UNKNOWN_LANGUAGE;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {string} tagName
|
|
* @return {!HTMLElement}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.createHTMLElement_ = function(tagName) {
|
|
const element =
|
|
/** @type {!HTMLElement} */ (document.createElement(tagName));
|
|
return element;
|
|
};
|
|
|
|
|
|
/**
|
|
* @const {string}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.SEEK_BAR_BASE_COLOR_ = 'rgba(255, 255, 255, 0.3)';
|
|
|
|
|
|
/**
|
|
* @const {string}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.SEEK_BAR_PLAYED_COLOR_ = 'rgb(255, 255, 255)';
|
|
|
|
|
|
/**
|
|
* @const {string}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.SEEK_BAR_BUFFERED_COLOR_ = 'rgba(255, 255, 255, 0.54)';
|
|
|
|
|
|
/**
|
|
* @const {string}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.VOLUME_BAR_VOLUME_LEVEL_COLOR_ = 'rgb(255, 255, 255)';
|
|
|
|
|
|
/**
|
|
* @const {string}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.VOLUME_BAR_BASE_COLOR_ = 'rgba(255, 255, 255, 0.54)';
|
|
|
|
|
|
/**
|
|
* @const {number}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR_ = 5; // seconds
|
|
|
|
|
|
/**
|
|
* @enum {number}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.Opacity_ = {
|
|
'TRANSPARENT': 0,
|
|
'OPAQUE': 1,
|
|
};
|
|
|
|
|
|
/**
|
|
* @const {number}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.KEYCODE_TAB_ = 9;
|
|
|
|
|
|
/**
|
|
* @const {number}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.KEYCODE_ESCAPE_ = 27;
|
|
|
|
|
|
/** @private {!Array.<string>} */
|
|
shaka.ui.Controls.controlPanelElements_ = [
|
|
'time_and_duration',
|
|
'mute',
|
|
'volume',
|
|
'fullscreen',
|
|
'overflow_menu',
|
|
'rewind',
|
|
'fast_forward',
|
|
];
|
|
|
|
|
|
/** @private {!Array.<string>} */
|
|
shaka.ui.Controls.overflowButtons_ = [
|
|
'captions',
|
|
'cast',
|
|
'quality',
|
|
'language',
|
|
'picture_in_picture',
|
|
];
|
|
|
|
|
|
/**
|
|
* @const {string}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.ARIA_LABEL_= 'aria-label';
|
|
|
|
|
|
/**
|
|
* These strings are used to insert material design icons
|
|
* and should never be localized.
|
|
* @enum {string}
|
|
* @private
|
|
*/
|
|
shaka.ui.Controls.MaterialDesignIcons_ = {
|
|
'FULLSCREEN': 'fullscreen',
|
|
'EXIT_FULLSCREEN': 'fullscreen_exit',
|
|
'CLOSED_CAPTIONS': 'closed_caption',
|
|
'CHECKMARK': 'done',
|
|
'LANGUAGE': 'language',
|
|
'PIP': 'picture_in_picture_alt',
|
|
// 'branding_watermark' material icon looks like a "dark version"
|
|
// of the p-i-p icon. We use "dark version" icons to signal that the
|
|
// feature is turned on.
|
|
'EXIT_PIP': 'branding_watermark',
|
|
'BACK': 'arrow_back',
|
|
'RESOLUTION': 'settings',
|
|
'MUTE': 'volume_up',
|
|
'UNMUTE': 'volume_off',
|
|
'CAST': 'cast',
|
|
'EXIT_CAST': 'cast_connected',
|
|
'OPEN_OVERFLOW': 'more_vert',
|
|
'REWIND': 'fast_rewind',
|
|
'FAST_FORWARD': 'fast_forward',
|
|
};
|