/**
* @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.OverflowMenu');
goog.require('mozilla.LanguageMapping');
goog.require('shaka.ui.Constants');
goog.require('shaka.ui.Element');
goog.require('shaka.ui.Enums');
goog.require('shaka.ui.Locales');
goog.require('shaka.ui.Localization');
goog.require('shaka.ui.Utils');
/**
* @extends {shaka.ui.Element}
* @final
* @export
*/
shaka.ui.OverflowMenu = class extends shaka.ui.Element {
/**
* @param {!HTMLElement} parent
* @param {!shaka.ui.Controls} controls
*/
constructor(parent, controls) {
super(parent, controls);
/** @private {!shaka.extern.UIConfiguration} */
this.config_ = this.controls.getConfig();
/** @private {!HTMLMediaElement} */
this.localVideo_ = this.controls.getLocalVideo();
/** @private {!shaka.cast.CastProxy} */
this.castProxy_ = this.controls.getCastProxy();
this.initOptionalElementsToNull_();
/** @private {!Map.<string, !Function>} */
this.elementNamesToFunctions_ = new Map([
['captions', () => { this.addCaptionButton_(); }],
['cast', () => { this.addCastButton_(); }],
['quality', () => { this.addResolutionButton_(); }],
['language', () => { this.addLanguagesButton_(); }],
['picture_in_picture', () => { this.addPipButton_(); }],
]);
/** @private {!HTMLElement} */
this.controlsContainer_ = this.controls.getControlsContainer();
this.addOverflowMenuButton_();
this.addOverflowMenu_();
/** @private {!NodeList.<!Element>} */
this.backToOverflowMenuButtons_ = this.controls.getVideoContainer().
getElementsByClassName('shaka-back-to-overflow-button');
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.controls.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.controls.overrideCssShowControls();
});
}
this.eventManager.listen(
this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => {
this.updateLocalizedStrings_();
});
this.eventManager.listen(
this.localization, shaka.ui.Localization.LOCALE_CHANGED, () => {
this.updateLocalizedStrings_();
});
this.eventManager.listen(
this.localVideo_, 'enterpictureinpicture', () => {
this.onEnterPictureInPicture_();
});
this.eventManager.listen(
this.localVideo_, 'leavepictureinpicture', () => {
this.onLeavePictureInPicture_();
});
if (this.castButton_) {
this.eventManager.listen(
this.castButton_, 'click', () => {
this.onCastClick_();
});
}
if (this.captionButton_) {
this.eventManager.listen(
this.captionButton_, 'click', () => {
this.onCaptionClick_();
});
}
if (this.pipButton_) {
this.eventManager.listen(
this.pipButton_, 'click', () => {
this.onPipClick_();
});
}
this.eventManager.listen(
this.player, 'texttrackvisibility', () => {
this.onCaptionStateChange_();
});
this.eventManager.listen(
this.player, 'trackschanged', () => {
this.onTracksChange_();
});
this.eventManager.listen(
this.player, 'variantchanged', () => {
this.onVariantChange_();
});
this.eventManager.listen(
this.player, 'textchanged', () => {
this.updateTextLanguages_();
});
this.eventManager.listen(
this.overflowMenu_, 'touchstart', (event) => {
this.controls.setLastTouchEventTime(Date.now());
event.stopPropagation();
});
this.eventManager.listen(
this.overflowMenuButton_, 'click', () => {
this.onOverflowMenuButtonClick_();
});
if (this.resolutionButton_) {
this.eventManager.listen(
this.resolutionButton_, 'click', () => {
this.onResolutionClick_();
});
}
if (this.languagesButton_) {
this.eventManager.listen(
this.languagesButton_, 'click', () => {
this.onLanguagesClick_();
});
}
this.eventManager.listen(
this.controls, 'caststatuschange', (e) => {
this.onCastStatusChange_(e);
});
this.eventManager.listen(
this.controlsContainer_, 'touchstart', (event) => {
// 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();
}
});
// Initialize caption state with a fake event.
this.onCaptionStateChange_();
const LocIds = shaka.ui.Locales.Ids;
/** @private {!Map.<HTMLElement, string>} */
this.ariaLabels_ = new Map()
.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.resolutionButton_, LocIds.ARIA_LABEL_RESOLUTION)
.set(this.languagesButton_, LocIds.ARIA_LABEL_LANGUAGE)
.set(this.castButton_, LocIds.ARIA_LABEL_CAST)
.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);
// Set all the localized strings with currently preferred language
this.updateLocalizedStrings_();
}
/**
* @private
*/
initOptionalElementsToNull_() {
/** @private {HTMLElement} */
this.captionButton_ = null;
/** @private {HTMLElement} */
this.captionIcon_ = null;
/** @private {HTMLElement} */
this.castButton_ = null;
/** @private {HTMLElement} */
this.castIcon_ = null;
/** @private {HTMLElement} */
this.overflowMenuButton_ = 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
*/
addOverflowMenu_() {
/** @private {!HTMLElement} */
this.overflowMenu_ = shaka.ui.Utils.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)) {
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
*/
addOverflowMenuButton_() {
this.overflowMenuButton_ = shaka.ui.Utils.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.Enums.MaterialDesignIcons.OPEN_OVERFLOW;
this.parent.appendChild(this.overflowMenuButton_);
}
/**
* @private
*/
addCaptionButton_() {
this.captionButton_ = shaka.ui.Utils.createHTMLElement('button');
this.captionButton_.classList.add('shaka-caption-button');
this.captionIcon_ = shaka.ui.Utils.createHTMLElement('i');
this.captionIcon_.classList.add('material-icons');
this.captionIcon_.textContent =
shaka.ui.Enums.MaterialDesignIcons.CLOSED_CAPTIONS;
this.captionButton_.appendChild(this.captionIcon_);
const label = shaka.ui.Utils.createHTMLElement('label');
label.classList.add('shaka-overflow-button-label');
this.captionsNameSpan_ = shaka.ui.Utils.createHTMLElement('span');
label.appendChild(this.captionsNameSpan_);
this.currentCaptions_ = shaka.ui.Utils.createHTMLElement('span');
this.currentCaptions_.classList.add('shaka-current-selection-span');
label.appendChild(this.currentCaptions_);
this.captionButton_.appendChild(label);
this.overflowMenu_.appendChild(this.captionButton_);
}
/**
* @private
*/
addTextLangMenu_() {
this.textLangMenu_ = shaka.ui.Utils.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.Utils.createHTMLElement('button');
this.backFromCaptionsButton_.classList.add('shaka-back-to-overflow-button');
this.textLangMenu_.appendChild(this.backFromCaptionsButton_);
const backIcon = shaka.ui.Utils.createHTMLElement('i');
backIcon.classList.add('material-icons');
backIcon.textContent = shaka.ui.Enums.MaterialDesignIcons.BACK;
this.backFromCaptionsButton_.appendChild(backIcon);
this.backFromCaptionsSpan_ = shaka.ui.Utils.createHTMLElement('span');
this.backFromCaptionsButton_.appendChild(this.backFromCaptionsSpan_);
// Add the off option
const off = shaka.ui.Utils.createHTMLElement('button');
off.setAttribute('aria-selected', 'true');
this.textLangMenu_.appendChild(off);
const chosenIcon = shaka.ui.Utils.createHTMLElement('i');
chosenIcon.classList.add('material-icons');
chosenIcon.classList.add('shaka-chosen-item');
// This text content is actually a material design icon.
chosenIcon.textContent = shaka.ui.Enums.MaterialDesignIcons.CHECKMARK;
// Screen reader should ignore 'done'.
chosenIcon.setAttribute('aria-hidden', 'true');
off.appendChild(chosenIcon);
this.captionsOffSpan_ = shaka.ui.Utils.createHTMLElement('span');
this.captionsOffSpan_.classList.add('shaka-auto-span');
off.appendChild(this.captionsOffSpan_);
this.controlsContainer_.appendChild(this.textLangMenu_);
}
/**
* @private
*/
addCastButton_() {
this.castButton_ = shaka.ui.Utils.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.Utils.createHTMLElement('i');
this.castIcon_.classList.add('material-icons');
// This text content is actually a material design icon.
this.castIcon_.textContent = shaka.ui.Enums.MaterialDesignIcons.CAST;
this.castButton_.appendChild(this.castIcon_);
const label = shaka.ui.Utils.createHTMLElement('label');
label.classList.add('shaka-overflow-button-label');
this.castNameSpan_ = shaka.ui.Utils.createHTMLElement('span');
label.appendChild(this.castNameSpan_);
this.castCurrentSelectionSpan_ =
shaka.ui.Utils.createHTMLElement('span');
this.castCurrentSelectionSpan_.classList.add(
'shaka-current-selection-span');
label.appendChild(this.castCurrentSelectionSpan_);
this.castButton_.appendChild(label);
this.overflowMenu_.appendChild(this.castButton_);
}
/**
* @private
*/
addResolutionMenu_() {
this.resolutionMenu_ = shaka.ui.Utils.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.Utils.createHTMLElement('button');
this.backFromResolutionButton_.classList.add(
'shaka-back-to-overflow-button');
this.resolutionMenu_.appendChild(this.backFromResolutionButton_);
const backIcon = shaka.ui.Utils.createHTMLElement('i');
backIcon.classList.add('material-icons');
backIcon.textContent = shaka.ui.Enums.MaterialDesignIcons.BACK;
this.backFromResolutionButton_.appendChild(backIcon);
this.backFromResolutionSpan_ = shaka.ui.Utils.createHTMLElement('span');
this.backFromResolutionButton_.appendChild(this.backFromResolutionSpan_);
// Add the abr option
const auto = shaka.ui.Utils.createHTMLElement('button');
auto.setAttribute('aria-selected', 'true');
this.resolutionMenu_.appendChild(auto);
const chosenIcon = shaka.ui.Utils.createHTMLElement('i');
chosenIcon.classList.add('material-icons');
chosenIcon.classList.add('shaka-chosen-item');
chosenIcon.textContent = shaka.ui.Enums.MaterialDesignIcons.CHECKMARK;
// Screen reader should ignore the checkmark.
chosenIcon.setAttribute('aria-hidden', 'true');
auto.appendChild(chosenIcon);
this.abrOnSpan_ = shaka.ui.Utils.createHTMLElement('span');
this.abrOnSpan_.classList.add('shaka-auto-span');
auto.appendChild(this.abrOnSpan_);
this.controlsContainer_.appendChild(this.resolutionMenu_);
}
/**
* @private
*/
addResolutionButton_() {
this.resolutionButton_ = shaka.ui.Utils.createHTMLElement('button');
this.resolutionButton_.classList.add('shaka-resolution-button');
const icon = shaka.ui.Utils.createHTMLElement('i');
icon.classList.add('material-icons');
icon.textContent = shaka.ui.Enums.MaterialDesignIcons.RESOLUTION;
this.resolutionButton_.appendChild(icon);
const label = shaka.ui.Utils.createHTMLElement('label');
label.classList.add('shaka-overflow-button-label');
this.resolutionNameSpan_ = shaka.ui.Utils.createHTMLElement('span');
label.appendChild(this.resolutionNameSpan_);
this.currentResolution_ = shaka.ui.Utils.createHTMLElement('span');
this.currentResolution_.classList.add('shaka-current-selection-span');
label.appendChild(this.currentResolution_);
this.resolutionButton_.appendChild(label);
this.overflowMenu_.appendChild(this.resolutionButton_);
}
/**
* @private
*/
addAudioLangMenu_() {
this.audioLangMenu_ = shaka.ui.Utils.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.Utils.createHTMLElement('button');
this.backFromLanguageButton_.classList.add('shaka-back-to-overflow-button');
this.audioLangMenu_.appendChild(this.backFromLanguageButton_);
const backIcon = shaka.ui.Utils.createHTMLElement('i');
backIcon.classList.add('material-icons');
backIcon.textContent = shaka.ui.Enums.MaterialDesignIcons.BACK;
this.backFromLanguageButton_.appendChild(backIcon);
this.backFromLanguageSpan_ = shaka.ui.Utils.createHTMLElement('span');
this.backFromLanguageButton_.appendChild(this.backFromLanguageSpan_);
this.controlsContainer_.appendChild(this.audioLangMenu_);
}
/**
* @private
*/
addLanguagesButton_() {
this.languagesButton_ = shaka.ui.Utils.createHTMLElement('button');
this.languagesButton_.classList.add('shaka-language-button');
const icon = shaka.ui.Utils.createHTMLElement('i');
icon.classList.add('material-icons');
icon.textContent = shaka.ui.Enums.MaterialDesignIcons.LANGUAGE;
this.languagesButton_.appendChild(icon);
const label = shaka.ui.Utils.createHTMLElement('label');
label.classList.add('shaka-overflow-button-label');
this.languageNameSpan_ = shaka.ui.Utils.createHTMLElement('span');
this.languageNameSpan_.classList.add('languageSpan');
label.appendChild(this.languageNameSpan_);
this.currentAudioLanguage_ = shaka.ui.Utils.createHTMLElement('span');
this.currentAudioLanguage_.classList.add('shaka-current-selection-span');
const language = this.player.getConfiguration().preferredAudioLanguage;
this.currentAudioLanguage_.textContent = this.getLanguageName_(language);
label.appendChild(this.currentAudioLanguage_);
this.languagesButton_.appendChild(label);
this.overflowMenu_.appendChild(this.languagesButton_);
}
/**
* @private
*/
addPipButton_() {
const LocIds = shaka.ui.Locales.Ids;
this.pipButton_ = shaka.ui.Utils.createHTMLElement('button');
this.pipButton_.classList.add('shaka-pip-button');
this.pipIcon_ = shaka.ui.Utils.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.Enums.MaterialDesignIcons.PIP;
this.pipButton_.appendChild(this.pipIcon_);
const label = shaka.ui.Utils.createHTMLElement('label');
label.classList.add('shaka-overflow-button-label');
this.pipNameSpan_ = shaka.ui.Utils.createHTMLElement('span');
this.pipNameSpan_.textContent =
this.localization.resolve(LocIds.LABEL_PICTURE_IN_PICTURE);
label.appendChild(this.pipNameSpan_);
this.currentPipState_ = shaka.ui.Utils.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
*/
isPipAllowed_() {
return document.pictureInPictureEnabled &&
!this.video.disablePictureInPicture;
}
/** @private */
onCaptionClick_() {
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 */
onResolutionClick_() {
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 */
onLanguagesClick_() {
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 */
onTracksChange_() {
// 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 */
onVariantChange_() {
// Update language and resolution selections
this.updateResolutionSelection_();
this.updateAudioLanguages_();
}
/** @private */
updateResolutionSelection_() {
// 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.Utils.createHTMLElement('button');
button.classList.add('explicit-resolution');
button.addEventListener('click',
this.onTrackSelected_.bind(this, track));
let span = shaka.ui.Utils.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.Utils.createHTMLElement('button');
autoButton.addEventListener('click', function() {
let config = {abr: {enabled: true}};
this.player.configure(config);
this.updateResolutionSelection_();
}.bind(this));
let autoSpan = shaka.ui.Utils.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 */
updateAudioLanguages_() {
// 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 */
updateTextLanguages_() {
// 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.Utils.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
*/
updateLanguages_(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.Utils.createHTMLElement('button');
button.addEventListener('click', onLanguageSelected.bind(this, language));
let span = shaka.ui.Utils.createHTMLElement('span');
span.textContent = this.getLanguageName_(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);
});
}
/**
* Returns the language's name for itself in its own script (autoglottonym),
* if we have it.
*
* If the locale, including region, can be mapped to a name, we return a very
* specific name including the region. For example, "de-AT" would map to
* "Deutsch (Österreich)" or Austrian German.
*
* If only the language part of the locale is in our map, we append the locale
* itself for specificity. For example, "ar-EG" (Egyptian Arabic) would map
* to "ﺎﻠﻋﺮﺒﻳﺓ (ar-EG)". In this way, multiple versions of Arabic whose
* regions are not in our map would not all look the same in the language
* list, but could be distinguished by their locale.
*
* Finally, if language part of the locale is not in our map, we label it
* "unknown", as translated to the UI locale, and we append the locale itself
* for specificity. For example, "sjn" would map to "Unknown (sjn)". In this
* way, multiple unrecognized languages would not all look the same in the
* language list, but could be distinguished by their locale.
*
* @param {string} locale
* @return {string} The language's name for itself in its own script, or as
* close as we can get with the information we have.
* @private
*/
getLanguageName_(locale) {
if (!locale) {
return '';
}
// Shorthand for resolving a localization ID.
const resolve = (id) => this.localization.resolve(id);
// Handle some special cases first. These are reserved language tags that
// are used to indicate something that isn't one specific language.
switch (locale) {
case 'mul':
return resolve(shaka.ui.Locales.Ids.LABEL_MULTIPLE_LANGUAGES);
case 'zxx':
return resolve(shaka.ui.Locales.Ids.LABEL_NOT_APPLICABLE);
}
// Extract the base language from the locale as a fallback step.
const language = shaka.util.LanguageUtils.getBase(locale);
// First try to resolve the full language name.
// If that fails, try the base.
// Finally, report "unknown".
// When there is a loss of specificity (either to a base language or to
// "unknown"), we should append the original language code.
// Otherwise, there may be multiple identical-looking items in the list.
if (locale in mozilla.LanguageMapping) {
return mozilla.LanguageMapping[locale].nativeName;
} else if (language in mozilla.LanguageMapping) {
return mozilla.LanguageMapping[language].nativeName +
' (' + locale + ')';
} else {
return resolve(shaka.ui.Locales.Ids.LABEL_UNKNOWN_LANGUAGE) +
' (' + locale + ')';
}
}
/**
* @param {!shaka.extern.Track} track
* @private
*/
onTrackSelected_(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
*/
onAudioLanguageSelected_(language) {
this.player.selectAudioLanguage(language);
}
/**
* @param {string} language
* @return {!Promise}
* @private
*/
async onTextLanguageSelected_(language) {
await this.player.setTextTrackVisibility(true);
this.player.selectTextLanguage(language);
}
/**
* @param {HTMLElement} menu
* @private
*/
focusOnTheChosenItem_(menu) {
if (!menu) return;
const chosenItem = shaka.ui.Utils.getDescendantIfExists(
menu, 'shaka-chosen-item');
if (chosenItem) {
chosenItem.parentElement.focus();
}
}
/**
* @return {!Element}
* @private
*/
chosenIcon_() {
let chosenIcon = shaka.ui.Utils.createHTMLElement('i');
chosenIcon.classList.add('material-icons');
chosenIcon.textContent = shaka.ui.Enums.MaterialDesignIcons.CHECKMARK;
// Screen reader should ignore 'done'.
chosenIcon.setAttribute('aria-hidden', 'true');
return chosenIcon;
}
/** @private */
onCaptionStateChange_() {
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 */
async onCastClick_() {
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.controls.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
*/
async onPipClick_() {
try {
if (!document.pictureInPictureElement) {
await this.video.requestPictureInPicture();
} else {
await document.exitPictureInPicture();
}
} catch (error) {
this.controls.dispatchEvent(new shaka.util.FakeEvent('error', {
errorDetails: error,
}));
}
}
/** @private */
onEnterPictureInPicture_() {
const LocIds = shaka.ui.Locales.Ids;
this.pipIcon_.textContent = shaka.ui.Enums.MaterialDesignIcons.EXIT_PIP;
this.pipButton_.setAttribute(shaka.ui.Constants.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 */
onLeavePictureInPicture_() {
const LocIds = shaka.ui.Locales.Ids;
this.pipIcon_.textContent = shaka.ui.Enums.MaterialDesignIcons.PIP;
this.pipButton_.setAttribute(shaka.ui.Constants.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 */
onOverflowMenuButtonClick_() {
if (this.controls.anySettingsMenusAreOpen()) {
this.controls.hideSettingsMenus();
} else {
shaka.ui.Controls.setDisplay(this.overflowMenu_, true);
this.controls.overrideCssShowControls();
// 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();
}
}
}
/**
* @private
*/
setCurrentCastSelection_() {
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);
}
}
/**
* @private
*/
updateLocalizedStrings_() {
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(shaka.ui.Constants.ARIA_LABEL,
this.localization.resolve(id));
}
// Localize state-dependant labels
if (this.pipButton_) {
const pipAriaLabel = document.pictureInPictureElement ?
LocIds.ARIA_LABEL_EXIT_PICTURE_IN_PICTURE :
LocIds.ARIA_LABEL_ENTER_PICTURE_IN_PICTURE;
this.pipButton_.setAttribute(shaka.ui.Constants.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);
}
}
/**
* @param {Event} e
* @private
*/
onCastStatusChange_(e) {
const canCast = this.castProxy_.canCast() && this.controls.isCastAllowed();
const isCasting = e['newStatus'];
if (this.castButton_) {
const materialDesignIcons = shaka.ui.Enums.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) {
// Picture-in-picture is not applicable if we're casting
if (pipIsEnabled) {
shaka.ui.Controls.setDisplay(this.pipButton_, false);
}
} else {
if (pipIsEnabled) {
shaka.ui.Controls.setDisplay(this.pipButton_, true);
}
}
}
/**
* Resolve a special language code to a name/description enum.
*
* @param {string} lang
* @return {string}
*/
resolveSpecialLanguageCode_(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;
}
}
};
/**
* @implements {shaka.extern.IUIElement.Factory}
* @final
*/
shaka.ui.OverflowMenu.Factory = class {
/** @override */
create(rootElement, controls) {
return new shaka.ui.OverflowMenu(rootElement, controls);
}
};
shaka.ui.Controls.registerElement(
'overflow_menu', new shaka.ui.OverflowMenu.Factory());