feat(UI): Modernize the statistics panel (#10144)

New design:
<img width="866" height="733" alt="Design"
src="https://github.com/user-attachments/assets/403c2ece-3ee0-46de-a310-a1f6adae8929"
/>
This commit is contained in:
Álvaro Velad Galván
2026-05-28 20:45:43 +02:00
committed by GitHub
parent 98a197d689
commit 686d550a3a
5 changed files with 292 additions and 88 deletions
+31 -13
View File
@@ -1290,22 +1290,40 @@ describe('UI', () => {
it('displays all the available statistics', () => {
const skippedStats = ['stateHistory', 'switchHistory'];
const nodes = statisticsContainer.childNodes;
// First index is close button.
let nodeIndex = 1;
/**
* Returns the stat node by label name.
* @param {string} name
* @return {?Node}
*/
function getStatsElementByName(name) {
const nodes = statisticsContainer.childNodes;
for (const node of nodes) {
if (node.hasChildNodes() &&
node.childNodes.length >= 2 &&
node.childNodes[0].textContent.replace(':', '') == name) {
return node;
}
}
return null;
}
for (const statistic in new shaka.util.Stats().getBlob()) {
if (!skippedStats.includes(statistic)) {
// Text content of label (without ':') is a valid statistic
const label = nodes[nodeIndex].childNodes[0].textContent;
expect(label.replace(':', '')).toBe(statistic);
// Value has been parsed and it is not the default 'NaN'
const value = nodes[nodeIndex].childNodes[1].textContent;
expect(value).not.toBe('NaN');
nodeIndex += 1;
if (skippedStats.includes(statistic)) {
continue;
}
const node = getStatsElementByName(statistic);
expect(node).not.toBe(null);
const label = node.childNodes[0].textContent;
expect(label.replace(':', '')).toBe(statistic);
const value = node.childNodes[1].textContent;
expect(value).not.toBe('NaN');
}
});
+33 -16
View File
@@ -83,6 +83,10 @@ shaka.ui.AdStatisticsButton = class extends shaka.ui.Element {
/** @private {!Map<string, HTMLElement>} */
this.displayedElements_ = new Map();
/** @private {!HTMLElement} */
this.headerTitle_ = shaka.util.Dom.createHTMLElement('span');
this.headerTitle_.classList.add('shaka-statistics-title');
const parseLoadTimes = (name) => {
let totalTime = 0;
const loadTimes =
@@ -109,13 +113,9 @@ shaka.ui.AdStatisticsButton = class extends shaka.ui.Element {
/** @private {shaka.util.Timer} */
this.timer_ = new shaka.util.Timer(() => {
this.onTimerTick_();
this.updateStats_();
});
this.updateLocalizedStrings();
this.loadContainer_();
this.eventManager.listen(this.button_, 'click', () => {
if (!this.controls.isOpaque()) {
return;
@@ -125,13 +125,20 @@ shaka.ui.AdStatisticsButton = class extends shaka.ui.Element {
});
this.eventManager.listen(this.player, 'loading', () => {
shaka.ui.Utils.setDisplay(this.button_, false);
this.updateStats_();
this.checkAvailability();
});
this.eventManager.listen(
this.adManager, shaka.ads.Utils.AD_STARTED, () => {
shaka.ui.Utils.setDisplay(this.button_, true);
this.updateStats_();
this.checkAvailability();
});
this.updateLocalizedStrings();
this.updateStats_();
this.loadContainer_();
this.checkAvailability();
}
/** @private */
@@ -156,6 +163,9 @@ shaka.ui.AdStatisticsButton = class extends shaka.ui.Element {
this.nameSpan_.textContent =
this.localization.resolve(LocIds.AD_STATISTICS);
this.headerTitle_.textContent =
this.localization.resolve(LocIds.AD_STATISTICS);
this.button_.ariaLabel = this.localization.resolve(LocIds.AD_STATISTICS);
const labelText = this.container_.classList.contains('shaka-hidden') ?
@@ -165,9 +175,15 @@ shaka.ui.AdStatisticsButton = class extends shaka.ui.Element {
/** @override */
checkAvailability() {
shaka.ui.Utils.setDisplay(
this.button_,
!this.isSubMenuOpened && this.currentStats_.started > 0);
const hasStats = this.currentStats_.started > 0 ||
this.currentStats_.overlayAds > 0 ||
this.currentStats_.playedCompletely > 0 ||
this.currentStats_.skipped > 0 ||
this.currentStats_.errors > 0;
shaka.ui.Utils.setDisplay(this.button_, !this.isSubMenuOpened && hasStats);
if (hasStats && !this.container_.classList.contains('shaka-hidden')) {
this.onClick_();
}
}
/**
@@ -193,15 +209,16 @@ shaka.ui.AdStatisticsButton = class extends shaka.ui.Element {
/** @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,
const header = shaka.util.Dom.createHTMLElement('div');
header.classList.add('shaka-statistics-header');
header.classList.add('shaka-no-propagation');
header.appendChild(this.headerTitle_);
const icon = new shaka.ui.Icon(header,
shaka.ui.Enums.MaterialDesignSVGIcons['CLOSE']);
const iconElement = icon.getSvgElement();
iconElement.classList.add('material-icons', 'notranslate');
this.container_.appendChild(closeElement);
this.container_.appendChild(header);
this.eventManager.listen(iconElement, 'click', () => {
this.onClick_();
});
@@ -216,7 +233,7 @@ shaka.ui.AdStatisticsButton = class extends shaka.ui.Element {
}
/** @private */
onTimerTick_() {
updateStats_() {
this.currentStats_ = this.adManager.getStats();
for (const name of this.statisticsList_) {
+81 -47
View File
@@ -463,78 +463,112 @@
display: none;
}
.shaka-statistics-container {
overflow-x: hidden;
overflow-y: auto;
scrollbar-color: @general-font-color @general-background-color;
scrollbar-width: thin;
min-width: 300px;
color: @general-font-color;
background-color: rgba(35, 35, 35, 90%);
font-size: @general-font-size;
padding: 5px 10px;
border-radius: 2px;
position: absolute;
z-index: 2;
left: 15px;
top: 15px;
max-height: calc(100% - 115px);
/* Fades out with the other controls. */
.show-when-controls-shown();
div {
display: flex;
justify-content: space-between;
}
span {
color: rgb(150, 150, 150);
}
}
.shaka-statistics-container,
.shaka-ad-statistics-container {
overflow-x: hidden;
overflow-y: auto;
scrollbar-color: @general-font-color @general-background-color;
scrollbar-color: rgba(255, 255, 255, 15%) transparent;
scrollbar-width: thin;
min-width: 150px;
color: @general-font-color;
background-color: rgba(35, 35, 35, 90%);
background-color: rgba(12, 14, 18, 88%);
border: 1px solid rgba(255, 255, 255, 8%);
box-shadow: 0 4px 24px rgba(0, 0, 0, 50%);
font-size: @general-font-size;
font-family: monospace;
padding: 5px 10px;
border-radius: 2px;
padding: 0;
border-radius: 10px;
position: absolute;
z-index: 2;
right: 15px;
top: 15px;
max-height: calc(100% - 115px);
/* Fades out with the other controls. */
.show-when-controls-shown();
/* Header bar (close button row) */
.shaka-statistics-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px 8px;
border-bottom: 1px solid rgba(255, 255, 255, 7%);
svg {
width: 16px;
height: 16px;
opacity: 0.35;
cursor: pointer;
transition: opacity 0.15s;
&:hover {
opacity: 1;
}
}
}
/* Section group labels */
.shaka-statistics-section-label {
display: block;
padding: 8px 14px 3px;
font-size: 9px;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: rgba(255, 255, 255, 70%);
}
/* Dividers between groups */
.shaka-statistics-divider {
display: block;
height: 1px;
background: rgba(255, 255, 255, 6%);
margin: 6px 14px;
padding: 0;
}
/* Stat rows */
div {
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 14px;
gap: 16px;
transition: background 0.1s;
&:hover {
background: rgba(255, 255, 255, 3%);
}
}
span {
color: rgb(150, 150, 150);
/* Row labels */
label {
font-size: 11px;
color: rgba(255, 255, 255, 45%);
white-space: nowrap;
}
/* Row values */
span {
font-size: 11px;
color: rgba(255, 255, 255, 90%);
font-variant-numeric: tabular-nums;
text-align: right;
}
}
.shaka-statistics-container {
min-width: 280px;
left: 15px;
}
.shaka-ad-statistics-container {
min-width: 220px;
right: 15px;
}
.shaka-scrim-container {
+145 -12
View File
@@ -83,6 +83,9 @@ shaka.ui.StatisticsButton = class extends shaka.ui.Element {
/** @private {!Map<string, HTMLElement>} */
this.displayedElements_ = new Map();
/** @private {!HTMLElement} */
this.headerTitle_ = shaka.util.Dom.createHTMLElement('span');
this.headerTitle_.classList.add('shaka-statistics-title');
const parsePx = (name) => {
return this.currentStats_[name] + ' (px)';
@@ -173,7 +176,7 @@ shaka.ui.StatisticsButton = class extends shaka.ui.Element {
/** @private {shaka.util.Timer} */
this.timer_ = new shaka.util.Timer(() => {
this.onTimerTick_();
this.updateStats_();
});
this.updateLocalizedStrings();
@@ -208,6 +211,9 @@ shaka.ui.StatisticsButton = class extends shaka.ui.Element {
this.nameSpan_.textContent =
this.localization.resolve(LocIds.STATISTICS);
this.headerTitle_.textContent =
this.localization.resolve(LocIds.STATISTICS);
this.button_.ariaLabel = this.localization.resolve(LocIds.STATISTICS);
const labelText = this.container_.classList.contains('shaka-hidden') ?
@@ -238,30 +244,157 @@ shaka.ui.StatisticsButton = class extends shaka.ui.Element {
/** @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,
const header = shaka.util.Dom.createHTMLElement('div');
header.classList.add('shaka-statistics-header');
header.classList.add('shaka-no-propagation');
header.appendChild(this.headerTitle_);
const icon = new shaka.ui.Icon(header,
shaka.ui.Enums.MaterialDesignSVGIcons['CLOSE']);
const iconElement = icon.getSvgElement();
iconElement.classList.add('material-icons', 'notranslate');
this.container_.appendChild(closeElement);
this.container_.appendChild(header);
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);
/**
* @const {!Array<{label: string, stats: !Array<string>}>}
*/
const groups = [
{
label: 'Video',
stats: [
'width',
'height',
'currentCodecs',
],
},
{
label: 'Network',
stats: [
'estimatedBandwidth',
'streamBandwidth',
'maxSegmentDuration',
'bytesDownloaded',
],
},
{
label: 'Load',
stats: [
'loadLatency',
'timeToFirstFrame',
'manifestTimeSeconds',
'drmTimeSeconds',
'licenseTime',
],
},
{
label: 'Playback',
stats: [
'playTime',
'pauseTime',
'bufferingTime',
'liveLatency',
'completionPercent',
],
},
{
label: 'Frames',
stats: [
'decodedFrames',
'droppedFrames',
'corruptedFrames',
],
},
{
label: 'Stability',
stats: [
'stallsDetected',
'gapsJumped',
'nonFatalErrorCount',
],
},
{
label: 'Manifest',
stats: [
'manifestSizeBytes',
'manifestPeriodCount',
'manifestGapCount',
],
},
];
const configList = this.controls.getConfig().statisticsList;
/** @type {!Set<string>} Track which stats have already been rendered */
const rendered = new Set();
/** @param {string} labelText */
const appendSectionLabel = (labelText) => {
const el = shaka.util.Dom.createHTMLElement('div');
el.classList.add('shaka-statistics-section-label');
el.textContent = labelText;
this.container_.appendChild(el);
};
const appendDivider = () => {
const el = shaka.util.Dom.createHTMLElement('div');
el.classList.add('shaka-statistics-divider');
this.container_.appendChild(el);
};
let firstGroup = true;
for (const group of groups) {
// Only render stats that the integrator has configured AND that the
// player is actually reporting (skipping stateHistory / switchHistory).
const groupStats = group.stats.filter((name) =>
configList.includes(name) &&
name in this.currentStats_ &&
!this.skippedStats_.includes(name));
if (groupStats.length === 0) {
continue;
}
if (!firstGroup) {
appendDivider();
}
firstGroup = false;
appendSectionLabel(group.label);
for (const name of groupStats) {
this.container_.appendChild(this.generateComponent_(name));
this.statisticsList_.push(name);
} else {
rendered.add(name);
}
}
// Any configured stat not covered by the groups above goes into 'Other'.
const remaining = configList.filter((name) =>
!rendered.has(name) &&
name in this.currentStats_ &&
!this.skippedStats_.includes(name));
if (remaining.length > 0) {
appendDivider();
appendSectionLabel('Other');
for (const name of remaining) {
this.container_.appendChild(this.generateComponent_(name));
this.statisticsList_.push(name);
}
}
for (const name of configList) {
if (!(name in this.currentStats_)) {
shaka.log.alwaysWarn('Unrecognized statistic element:', name);
}
}
}
/** @private */
onTimerTick_() {
updateStats_() {
this.currentStats_ = this.player.getStats();
for (const name of this.statisticsList_) {
+2
View File
@@ -345,6 +345,8 @@ shaka.ui.Overlay = class {
'nonFatalErrorCount',
'manifestPeriodCount',
'manifestGapCount',
'gapsJumped',
'stallsDetected',
],
adStatisticsList: [
'loadTimes',