diff --git a/build/types/ui b/build/types/ui index 803cdd4e6b..e598b189f0 100644 --- a/build/types/ui +++ b/build/types/ui @@ -3,6 +3,7 @@ +../../third_party/language-mapping-list/language-mapping-list.js +../../ui/ad_counter.js +../../ui/ad_position.js ++../../ui/ad_statistics_button.js +../../ui/audio_language_selection.js +../../ui/externs/ui.js +../../ui/play_button.js diff --git a/docs/tutorials/ui-customization.md b/docs/tutorials/ui-customization.md index 6bba41fa22..6940c195af 100644 --- a/docs/tutorials/ui-customization.md +++ b/docs/tutorials/ui-customization.md @@ -99,11 +99,12 @@ The following buttons can be added to the overflow menu: supports AirPlay. * remote: adds a button that opens a Remote Playback dialog. The button is visible only if the browser supports Remote Playback API. -* Statistics: adds a button that displays statistics of the video. +* statistics: adds a button that displays statistics of the video. * recenter_vr: adds a button that recenter the VR view to the initial view. The button is visible only if playing a VR content. * toggle_stereoscopic: adds a button that toggle between monoscopic and stereoscopic. The button is visible only if playing a VR content. +* ad_statistics: adds a button that displays ad statistics of the video. Example: @@ -134,11 +135,12 @@ ui.configure(config); A custom context menu can be added through the `customContextMenu` boolean. Additionally, the `contextMenuElements` option can be used to add elements to it. The following buttons can be added to the context menu: -* Statistics: adds a button that displays statistics of the video. +* statistics: adds a button that displays statistics of the video. * loop: adds a button that controls if the currently selected video is played in a loop. * picture_in_picture: adds a button that enables/disables picture-in-picture mode on browsers that support it. Button is invisible on other browsers. Note that it will use the [Document Picture-in-Picture API]() if supported. +* ad_statistics: adds a button that displays ad statistics of the video. Example: ```js @@ -164,6 +166,21 @@ const config = { ui.configure(config); ``` +#### Configuring Ad Statistics +The list of ad statistics that are displayed when toggling the ad statistics button can be customized by specifying a `adStatisticsList` on the configuration. All of the statistics from the {@link shaka.extern.AdsStats `AdsStats`} extern can be displayed. + +Example: +```js +// Add a context menu with the 'ad_statistics' button that displays a container with +// the current 'started' and 'playedCompletely' values. +const config = { + 'customContextMenu' : true, + 'contextMenuElements' : ['ad_statistics'], + 'adStatisticsList' : ['started', 'playedCompletely'], +} +ui.configure(config); +``` + The presence of the seek bar and the big play button in the center of the video element can be customized with `addSeekBar` and `addBigPlayButton` booleans in the config. diff --git a/shaka-player.uncompiled.js b/shaka-player.uncompiled.js index af99683cc8..91c51d596c 100644 --- a/shaka-player.uncompiled.js +++ b/shaka-player.uncompiled.js @@ -86,6 +86,7 @@ goog.require('shaka.ui.OverflowMenu'); goog.require('shaka.ui.AudioLanguageSelection'); goog.require('shaka.ui.AdCounter'); goog.require('shaka.ui.AdPosition'); +goog.require('shaka.ui.AdStatisticsButton'); goog.require('shaka.ui.AirPlayButton'); goog.require('shaka.ui.BigPlayButton'); goog.require('shaka.ui.CastButton'); diff --git a/ui/ad_statistics_button.js b/ui/ad_statistics_button.js new file mode 100644 index 0000000000..a49af8a40e --- /dev/null +++ b/ui/ad_statistics_button.js @@ -0,0 +1,235 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + + +goog.provide('shaka.ui.AdStatisticsButton'); + +goog.require('shaka.log'); +goog.require('shaka.ads.AdManager'); +goog.require('shaka.ui.ContextMenu'); +goog.require('shaka.ui.Controls'); +goog.require('shaka.ui.Element'); +goog.require('shaka.ui.Enums'); +goog.require('shaka.ui.Locales'); +goog.require('shaka.ui.Localization'); +goog.require('shaka.ui.OverflowMenu'); +goog.require('shaka.ui.Utils'); +goog.require('shaka.util.Dom'); +goog.require('shaka.util.Timer'); +goog.requireType('shaka.ui.Controls'); + + +/** + * @extends {shaka.ui.Element} + * @final + * @export + */ +shaka.ui.AdStatisticsButton = class extends shaka.ui.Element { + /** + * @param {!HTMLElement} parent + * @param {!shaka.ui.Controls} controls + */ + constructor(parent, controls) { + super(parent, controls); + + /** @private {!HTMLButtonElement} */ + this.button_ = shaka.util.Dom.createButton(); + this.button_.classList.add('shaka-ad-statistics-button'); + + /** @private {!HTMLElement} */ + this.icon_ = shaka.util.Dom.createHTMLElement('i'); + this.icon_.classList.add('material-icons-round'); + this.icon_.textContent = + shaka.ui.Enums.MaterialDesignIcons.STATISTICS_ON; + this.button_.appendChild(this.icon_); + + const label = shaka.util.Dom.createHTMLElement('label'); + label.classList.add('shaka-overflow-button-label'); + + /** @private {!HTMLElement} */ + this.nameSpan_ = shaka.util.Dom.createHTMLElement('span'); + label.appendChild(this.nameSpan_); + + /** @private {!HTMLElement} */ + this.stateSpan_ = shaka.util.Dom.createHTMLElement('span'); + this.stateSpan_.classList.add('shaka-current-selection-span'); + label.appendChild(this.stateSpan_); + + this.button_.appendChild(label); + + this.parent.appendChild(this.button_); + + /** @private {!HTMLElement} */ + this.container_ = shaka.util.Dom.createHTMLElement('div'); + this.container_.classList.add('shaka-no-propagation'); + this.container_.classList.add('shaka-show-controls-on-mouse-over'); + this.container_.classList.add('shaka-ad-statistics-container'); + this.container_.classList.add('shaka-hidden'); + + const controlsContainer = this.controls.getControlsContainer(); + controlsContainer.appendChild(this.container_); + + /** @private {!Array} */ + this.statisticsList_ = []; + + /** @private {!Object.)>} */ + this.currentStats_ = this.adManager.getStats(); + + /** @private {!Object.} */ + this.displayedElements_ = {}; + + const parseLoadTimes = (name) => { + let totalTime = 0; + const loadTimes = + /** @type {!Array.} */ (this.currentStats_[name]); + for (const loadTime of loadTimes) { + totalTime += parseFloat(loadTime); + } + return totalTime; + }; + + const showNumber = (name) => { + return this.currentStats_[name]; + }; + + /** @private {!Object.} */ + this.parseFrom_ = { + 'loadTimes': parseLoadTimes, + 'started': showNumber, + 'playedCompletely': showNumber, + 'skipped': showNumber, + }; + + /** @private {shaka.util.Timer} */ + this.timer_ = new shaka.util.Timer(() => { + this.onTimerTick_(); + }); + + this.updateLocalizedStrings_(); + + this.loadContainer_(); + + 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.button_, 'click', () => { + this.onClick_(); + this.updateLocalizedStrings_(); + }); + + this.eventManager.listen(this.player, 'loading', () => { + shaka.ui.Utils.setDisplay(this.button_, false); + }); + + this.eventManager.listen( + this.adManager, shaka.ads.AdManager.AD_STARTED, () => { + shaka.ui.Utils.setDisplay(this.button_, true); + }); + } + + /** @private */ + onClick_() { + shaka.ui.Utils.setDisplay(this.parent, false); + + if (this.container_.classList.contains('shaka-hidden')) { + this.icon_.textContent = + shaka.ui.Enums.MaterialDesignIcons.STATISTICS_OFF; + this.timer_.tickEvery(0.1); + shaka.ui.Utils.setDisplay(this.container_, true); + } else { + this.icon_.textContent = + shaka.ui.Enums.MaterialDesignIcons.STATISTICS_ON; + this.timer_.stop(); + shaka.ui.Utils.setDisplay(this.container_, false); + } + } + + /** @private */ + updateLocalizedStrings_() { + const LocIds = shaka.ui.Locales.Ids; + + this.nameSpan_.textContent = + this.localization.resolve(LocIds.AD_STATISTICS); + + this.button_.ariaLabel = this.localization.resolve(LocIds.AD_STATISTICS); + + const labelText = this.container_.classList.contains('shaka-hidden') ? + LocIds.OFF : LocIds.ON; + this.stateSpan_.textContent = this.localization.resolve(labelText); + } + + /** @private */ + generateComponent_(name) { + const section = shaka.util.Dom.createHTMLElement('div'); + + const label = shaka.util.Dom.createHTMLElement('label'); + label.textContent = name + ':'; + section.appendChild(label); + + const value = shaka.util.Dom.createHTMLElement('span'); + value.textContent = this.parseFrom_[name](name); + section.appendChild(value); + + this.displayedElements_[name] = value; + + return section; + } + + /** @private */ + loadContainer_() { + for (const name of this.controls.getConfig().adStatisticsList) { + if (name in this.currentStats_) { + this.container_.appendChild(this.generateComponent_(name)); + this.statisticsList_.push(name); + } else { + shaka.log.alwaysWarn('Unrecognized ad statistic element:', name); + } + } + } + + /** @private */ + onTimerTick_() { + this.currentStats_ = this.adManager.getStats(); + + for (const name of this.statisticsList_) { + this.displayedElements_[name].textContent = + this.parseFrom_[name](name); + } + } + + /** @override */ + release() { + this.timer_.stop(); + this.timer_ = null; + super.release(); + } +}; + + +/** + * @implements {shaka.extern.IUIElement.Factory} + * @final + */ +shaka.ui.AdStatisticsButton.Factory = class { + /** @override */ + create(rootElement, controls) { + return new shaka.ui.AdStatisticsButton(rootElement, controls); + } +}; + + +shaka.ui.OverflowMenu.registerElement( + 'ad_statistics', new shaka.ui.AdStatisticsButton.Factory()); + +shaka.ui.ContextMenu.registerElement( + 'ad_statistics', new shaka.ui.AdStatisticsButton.Factory()); diff --git a/ui/externs/ui.js b/ui/externs/ui.js index dc01aa0d57..7e059e9254 100644 --- a/ui/externs/ui.js +++ b/ui/externs/ui.js @@ -65,6 +65,7 @@ shaka.extern.UIVolumeBarColors; * overflowMenuButtons: !Array., * contextMenuElements: !Array., * statisticsList: !Array., + * adStatisticsList: !Array., * playbackRates: !Array., * fastForwardRates: !Array., * rewindRates: !Array., @@ -106,6 +107,8 @@ shaka.extern.UIVolumeBarColors; * The ordered list of buttons in the context menu. * @property {!Array.} statisticsList * The ordered list of statistics present in the statistics container. + * @property {!Array.} adStatisticsList + * The ordered list of ad statistics present in the ad statistics container. * @property {!Array.} playbackRates * The ordered list of rates for playback selection. * @property {!Array.} fastForwardRates diff --git a/ui/less/containers.less b/ui/less/containers.less index b797ba1018..56fba64460 100644 --- a/ui/less/containers.less +++ b/ui/less/containers.less @@ -248,6 +248,38 @@ } } +.shaka-ad-statistics-container { + overflow-x: hidden; + overflow-y: auto; + + min-width: 150px; + + color: white; + background-color: rgba(35 35 35 / 90%); + + font-size: 14px; + + padding: 5px 10px; + border-radius: 2px; + + position: absolute; + z-index: 2; + right: 15px; + top: 15px; + + /* Fades out with the other controls. */ + .show-when-controls-shown(); + + div { + display: flex; + justify-content: space-between; + } + + span { + color: rgb(150 150 150); + } +} + .shaka-context-menu { background-color: rgba(35 35 35 / 90%); diff --git a/ui/locales/en.json b/ui/locales/en.json index 94d0c2e016..87c3fb6cba 100644 --- a/ui/locales/en.json +++ b/ui/locales/en.json @@ -1,6 +1,7 @@ { "AD_DURATION": "Ad duration", "AD_PROGRESS": "Ad [AD_ON] of [NUM_ADS]", + "AD_STATISTICS": "Ad statistics", "AD_TIME": "Ad: [AD_TIME]", "AIRPLAY": "AirPlay", "AUTO_QUALITY": "Auto", diff --git a/ui/locales/es-419.json b/ui/locales/es-419.json index 7ebd6740b8..eb30cb6473 100644 --- a/ui/locales/es-419.json +++ b/ui/locales/es-419.json @@ -1,6 +1,7 @@ { "AD_DURATION": "Duración del anuncio", "AD_PROGRESS": "Anuncio [AD_ON] de [NUM_ADS]", + "AD_STATISTICS": "Estadísticas de anuncios", "AD_TIME": "Anuncio: [AD_TIME]", "AUTO_QUALITY": "Automático", "BACK": "Atrás", @@ -32,6 +33,7 @@ "SEEK": "Buscar", "SKIP_AD": "Omitir anuncio", "SKIP_TO_LIVE": "Adelantar hasta la transmisión en vivo", + "STATISTICS": "Estadísticas", "SUBTITLE_FORCED": "Forzado", "SURROUND": "Envolvente", "TOGGLE_STEREOSCOPIC": "Alternar estereoscópico", diff --git a/ui/locales/es.json b/ui/locales/es.json index 3ea024a035..ebe2fbda9d 100644 --- a/ui/locales/es.json +++ b/ui/locales/es.json @@ -1,6 +1,7 @@ { "AD_DURATION": "Duración del anuncio", "AD_PROGRESS": "Anuncio [AD_ON] de [NUM_ADS]", + "AD_STATISTICS": "Estadísticas de anuncios", "AD_TIME": "Anuncio: [AD_TIME]", "AUTO_QUALITY": "Automático", "BACK": "Atrás", @@ -32,6 +33,7 @@ "SEEK": "Buscar", "SKIP_AD": "Saltar anuncio", "SKIP_TO_LIVE": "Ir al vídeo en directo", + "STATISTICS": "Estadísticas", "SUBTITLE_FORCED": "Forzado", "SURROUND": "Envolvente", "TOGGLE_STEREOSCOPIC": "Alternar estereoscópico", diff --git a/ui/locales/source.json b/ui/locales/source.json index 59f22c7b80..adcee0ae20 100644 --- a/ui/locales/source.json +++ b/ui/locales/source.json @@ -7,6 +7,10 @@ "description": "The contents of the ad countdown element, describing what ad you are on in a sequence of ads.", "message": "Ad [AD_ON:1] of [NUM_ADS:6]" }, + "AD_STATISTICS": { + "description": "Label for a button that displays ad statistics.", + "message": "Ad statistics" + }, "AD_TIME": { "description": "The text content of a label that shows how much time is left in the current ad.", "message": "Ad: [AD_TIME:0:05/0:10]" diff --git a/ui/ui.js b/ui/ui.js index 5c62619533..a5bf072130 100644 --- a/ui/ui.js +++ b/ui/ui.js @@ -217,10 +217,17 @@ shaka.ui.Overlay = class { 'manifestPeriodCount', 'manifestGapCount', ], + adStatisticsList: [ + 'loadTimes', + 'started', + 'playedCompletely', + 'skipped', + ], contextMenuElements: [ 'loop', 'picture_in_picture', 'statistics', + 'ad_statistics', ], playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], fastForwardRates: [2, 4, 8, 1],