mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-13 15:46:46 +03:00
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:
committed by
GitHub
parent
98a197d689
commit
686d550a3a
+31
-13
@@ -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
@@ -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
@@ -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
@@ -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_) {
|
||||
|
||||
Reference in New Issue
Block a user