Files
shaka-player/ui/statistics_button.js
T
Phyo Wai Lin e7b1e5a0f6 feat(UI): Ability to change UI icons (#9115)
## How to register a custom icon?

**Icons need to be registered before initializing the player.**

Register a custom icon from  Material Symbols:

```js
shaka.ui.IconRegistry.register('<PATH_VALUE>');
```

Register a custom Icon from any icon set:

```js
shaka.ui.IconRegistry.register('<NAME>', {
  path: '<PATH_VALUE>',
  viewBox: '<VIEW_BOX>',
  size:  24, // optional
});
```

Register a custom Icon (that contains multiple paths) from any icon set:

```js
shaka.ui.IconRegistry.register('<NAME>', {
  path: ['<PATH_VALUE_1>', '<PATH_VALUE_2>'],
  viewBox: '<VIEW_BOX>',
  size:  24, // optional
});
```

Register a custom Icon using URL of the icon:

```js
shaka.ui.IconRegistry.register('<NAME>', {
  url: '<URL>',
  size:  24, // optional
});
```

Closes: #9045

---------

Co-authored-by: Álvaro Velad Galván <ladvan91@hotmail.com>
2025-09-19 11:59:17 +02:00

313 lines
9.2 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.ui.StatisticsButton');
goog.require('shaka.log');
goog.require('shaka.ui.ContextMenu');
goog.require('shaka.ui.Controls');
goog.require('shaka.ui.Element');
goog.require('shaka.ui.Enums');
goog.require('shaka.ui.Icon');
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.StatisticsButton = 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-statistics-button');
/** @private {!shaka.ui.Icon} */
this.icon_ = new shaka.ui.Icon(this.button_,
shaka.ui.Enums.MaterialDesignSVGIcons['STATISTICS_ON']);
const label = shaka.util.Dom.createHTMLElement('label');
label.classList.add('shaka-overflow-button-label');
label.classList.add('shaka-simple-overflow-button-label-inline');
/** @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-statistics-container');
this.container_.classList.add('shaka-hidden');
const controlsContainer = this.controls.getControlsContainer();
controlsContainer.appendChild(this.container_);
/** @private {!Array} */
this.statisticsList_ = [];
/** @private {!Array} */
this.skippedStats_ = ['stateHistory', 'switchHistory'];
/** @private {!shaka.extern.Stats} */
this.currentStats_ = this.player.getStats();
/** @private {!Map<string, HTMLElement>} */
this.displayedElements_ = new Map();
const parsePx = (name) => {
return this.currentStats_[name] + ' (px)';
};
const parseString = (name) => {
return this.currentStats_[name];
};
const parsePercent = (name) => {
return this.currentStats_[name] + ' (%)';
};
const parseFrames = (name) => {
return this.currentStats_[name] + ' (frames)';
};
const parseSeconds = (name) => {
return this.currentStats_[name].toFixed(2) + ' (s)';
};
const parseBits = (name) => {
return Math.round(this.currentStats_[name] / 1000) + ' (kbits/s)';
};
const parseTime = (name) => {
return shaka.ui.Utils.buildTimeString(
this.currentStats_[name], false) + ' (m)';
};
const parseGaps = (name) => {
return this.currentStats_[name] + ' (gaps)';
};
const parseStalls = (name) => {
return this.currentStats_[name] + ' (stalls)';
};
const parseErrors = (name) => {
return this.currentStats_[name] + ' (errors)';
};
const parsePeriods = (name) => {
return this.currentStats_[name] + ' (periods)';
};
const parseBytes = (name) => {
const bytes = parseInt(this.currentStats_[name], 10);
if (bytes > 2 * 1e9) {
return (bytes / 1e9).toFixed(2) + 'GB';
} else if (bytes > 1e6) {
return (bytes / 1e6).toFixed(2) + 'MB';
} else if (bytes > 1e3) {
return (bytes / 1e3).toFixed(2) + 'KB';
} else {
return bytes + 'B';
}
};
/** @private {!Map<string, function(string): string>} */
this.parseFrom_ = new Map()
.set('width', parsePx)
.set('height', parsePx)
.set('currentCodecs', parseString)
.set('completionPercent', parsePercent)
.set('bufferingTime', parseSeconds)
.set('drmTimeSeconds', parseSeconds)
.set('licenseTime', parseSeconds)
.set('liveLatency', parseSeconds)
.set('loadLatency', parseSeconds)
.set('manifestTimeSeconds', parseSeconds)
.set('estimatedBandwidth', parseBits)
.set('streamBandwidth', parseBits)
.set('maxSegmentDuration', parseSeconds)
.set('pauseTime', parseTime)
.set('playTime', parseTime)
.set('corruptedFrames', parseFrames)
.set('decodedFrames', parseFrames)
.set('droppedFrames', parseFrames)
.set('stallsDetected', parseStalls)
.set('gapsJumped', parseGaps)
.set('manifestSizeBytes', parseBytes)
.set('bytesDownloaded', parseBytes)
.set('nonFatalErrorCount', parseErrors)
.set('manifestPeriodCount', parsePeriods)
.set('manifestGapCount', parseGaps);
/** @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_();
});
}
/** @private */
onClick_() {
if (this.container_.classList.contains('shaka-hidden')) {
this.icon_.use(shaka.ui.Enums.MaterialDesignSVGIcons['STATISTICS_OFF']);
this.timer_.tickEvery(0.1);
shaka.ui.Utils.setDisplay(this.container_, true);
} else {
this.icon_.use(shaka.ui.Enums.MaterialDesignSVGIcons['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.STATISTICS);
this.button_.ariaLabel = this.localization.resolve(LocIds.STATISTICS);
const labelText = this.container_.classList.contains('shaka-hidden') ?
LocIds.OFF : LocIds.ON;
this.stateSpan_.textContent = this.localization.resolve(labelText);
}
/**
* @param {string} name
* @return {!HTMLElement}
* @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_.get(name)(name);
section.appendChild(value);
this.displayedElements_.set(name, value);
return section;
}
/** @private */
loadContainer_() {
const closeElement = shaka.util.Dom.createHTMLElement('div');
closeElement.classList.add('shaka-no-propagation');
closeElement.classList.add('shaka-statistics-close');
const icon = new shaka.ui.Icon(closeElement,
shaka.ui.Enums.MaterialDesignSVGIcons['CLOSE']);
const iconElement = icon.getSvgElement();
iconElement.classList.add('material-icons', 'notranslate');
this.container_.appendChild(closeElement);
this.eventManager.listen(iconElement, 'click', () => {
this.onClick_();
});
for (const name of this.controls.getConfig().statisticsList) {
if (name in this.currentStats_ && !this.skippedStats_.includes(name)) {
const element = this.generateComponent_(name);
this.container_.appendChild(element);
this.statisticsList_.push(name);
} else {
shaka.log.alwaysWarn('Unrecognized statistic element:', name);
}
}
}
/** @private */
onTimerTick_() {
this.currentStats_ = this.player.getStats();
for (const name of this.statisticsList_) {
const element = this.displayedElements_.get(name);
element.textContent = this.parseFrom_.get(name)(name);
if (element && element.parentElement) {
const value = this.currentStats_[name];
if (typeof value == 'string') {
shaka.ui.Utils.setDisplay(element.parentElement, value != '');
} else {
shaka.ui.Utils.setDisplay(element.parentElement, !isNaN(value));
}
}
}
}
/** @override */
release() {
this.timer_.stop();
this.timer_ = null;
super.release();
}
};
/**
* @implements {shaka.extern.IUIElement.Factory}
* @final
*/
shaka.ui.StatisticsButton.Factory = class {
/** @override */
create(rootElement, controls) {
return new shaka.ui.StatisticsButton(rootElement, controls);
}
};
shaka.ui.OverflowMenu.registerElement(
'statistics', new shaka.ui.StatisticsButton.Factory());
shaka.ui.ContextMenu.registerElement(
'statistics', new shaka.ui.StatisticsButton.Factory());