@mattebox/player-diagnostics 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +24 -0
- package/README.md +45 -0
- package/dist/cdn/mattebox-player-diagnostics.min.js +124 -0
- package/dist/charts.d.ts +35 -0
- package/dist/charts.d.ts.map +1 -0
- package/dist/charts.js +401 -0
- package/dist/charts.js.map +1 -0
- package/dist/element.d.ts +62 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +677 -0
- package/dist/element.js.map +1 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/asyncToGenerator.js +27 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/defineProperty.js +12 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/objectSpread2.js +25 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/toPrimitive.js +14 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/toPropertyKey.js +9 -0
- package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/typeof.js +11 -0
- package/dist/es2015/charts.js +380 -0
- package/dist/es2015/element.js +651 -0
- package/dist/es2015/format.js +38 -0
- package/dist/es2015/host.js +34 -0
- package/dist/es2015/icon.js +46 -0
- package/dist/es2015/index.js +15 -0
- package/dist/es2015/report.js +145 -0
- package/dist/es2015/sampler.js +196 -0
- package/dist/es2015/style.js +135 -0
- package/dist/es2015/support.js +351 -0
- package/dist/es2015/trace.js +99 -0
- package/dist/format.d.ts +20 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +45 -0
- package/dist/format.js.map +1 -0
- package/dist/host.d.ts +19 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +38 -0
- package/dist/host.js.map +1 -0
- package/dist/icon.d.ts +31 -0
- package/dist/icon.d.ts.map +1 -0
- package/dist/icon.js +44 -0
- package/dist/icon.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/report.d.ts +140 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +125 -0
- package/dist/report.js.map +1 -0
- package/dist/sampler.d.ts +68 -0
- package/dist/sampler.d.ts.map +1 -0
- package/dist/sampler.js +208 -0
- package/dist/sampler.js.map +1 -0
- package/dist/style.d.ts +10 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +133 -0
- package/dist/style.js.map +1 -0
- package/dist/support.d.ts +46 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +247 -0
- package/dist/support.js.map +1 -0
- package/dist/trace.d.ts +14 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +100 -0
- package/dist/trace.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { bitrate, clock, fixed, ranges, rendition, share } from "./format.js";
|
|
2
|
+
import { CHART_TABS, createCharts } from "./charts.js";
|
|
3
|
+
import { findPlayer, whenPlayer } from "./host.js";
|
|
4
|
+
import { icon } from "./icon.js";
|
|
5
|
+
import { buildReport } from "./report.js";
|
|
6
|
+
import { createSampler } from "./sampler.js";
|
|
7
|
+
import { STYLE } from "./style.js";
|
|
8
|
+
import _asyncToGenerator from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/asyncToGenerator.js";
|
|
9
|
+
import { probeSupport } from "./support.js";
|
|
10
|
+
//#region src/element.ts
|
|
11
|
+
const POLL_MS = 500;
|
|
12
|
+
const DRAW_MS = 250;
|
|
13
|
+
const WINDOW = 120;
|
|
14
|
+
const WINDOWS = [
|
|
15
|
+
30,
|
|
16
|
+
60,
|
|
17
|
+
120,
|
|
18
|
+
300,
|
|
19
|
+
600
|
|
20
|
+
];
|
|
21
|
+
const PANEL_WIDTH = 560;
|
|
22
|
+
/** Air between the panel's top and the picture's, and around it. */
|
|
23
|
+
const AIR = 8;
|
|
24
|
+
const TABS = [
|
|
25
|
+
"playback",
|
|
26
|
+
"charts",
|
|
27
|
+
"engine",
|
|
28
|
+
"browser"
|
|
29
|
+
];
|
|
30
|
+
const TAB_NAMES = {
|
|
31
|
+
playback: "Playback",
|
|
32
|
+
charts: "Charts",
|
|
33
|
+
engine: "Engine",
|
|
34
|
+
browser: "Browser"
|
|
35
|
+
};
|
|
36
|
+
const READY_STATES = [
|
|
37
|
+
"nothing",
|
|
38
|
+
"metadata",
|
|
39
|
+
"current data",
|
|
40
|
+
"future data",
|
|
41
|
+
"enough data"
|
|
42
|
+
];
|
|
43
|
+
const NETWORK_STATES = [
|
|
44
|
+
"empty",
|
|
45
|
+
"idle",
|
|
46
|
+
"loading",
|
|
47
|
+
"no source"
|
|
48
|
+
];
|
|
49
|
+
function el(tag, part, text) {
|
|
50
|
+
const node = document.createElement(tag);
|
|
51
|
+
node.setAttribute("part", part);
|
|
52
|
+
if (text !== void 0) node.textContent = text;
|
|
53
|
+
return node;
|
|
54
|
+
}
|
|
55
|
+
function button(part, text) {
|
|
56
|
+
const node = el("button", part, text);
|
|
57
|
+
node.type = "button";
|
|
58
|
+
return node;
|
|
59
|
+
}
|
|
60
|
+
/** A cell's text and its part: a tick, a cross, a dash, or the text itself. */
|
|
61
|
+
function cell(value) {
|
|
62
|
+
switch (value) {
|
|
63
|
+
case "yes": return ["✓", "cell ok"];
|
|
64
|
+
case "no": return ["✕", "cell bad"];
|
|
65
|
+
case "na": return ["–", "cell na"];
|
|
66
|
+
case "maybe": return ["maybe", "cell maybe"];
|
|
67
|
+
default: return [value, "cell"];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
var MbxDiagnostics = class extends HTMLElement {
|
|
71
|
+
static get observedAttributes() {
|
|
72
|
+
return [
|
|
73
|
+
"label",
|
|
74
|
+
"label-copy",
|
|
75
|
+
"label-copied",
|
|
76
|
+
"window"
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
constructor() {
|
|
80
|
+
super();
|
|
81
|
+
this.player = null;
|
|
82
|
+
this.cancel = null;
|
|
83
|
+
this.offs = [];
|
|
84
|
+
this.sampler = createSampler();
|
|
85
|
+
this.timer = void 0;
|
|
86
|
+
this.drawer = void 0;
|
|
87
|
+
this.copied = void 0;
|
|
88
|
+
this.tab = "playback";
|
|
89
|
+
this.chartTab = "buffer";
|
|
90
|
+
this.support = null;
|
|
91
|
+
this.probing = false;
|
|
92
|
+
const root = this.attachShadow({ mode: "open" });
|
|
93
|
+
const style = document.createElement("style");
|
|
94
|
+
style.textContent = STYLE;
|
|
95
|
+
this.toggle = button("button");
|
|
96
|
+
this.toggle.setAttribute("aria-haspopup", "dialog");
|
|
97
|
+
this.toggle.setAttribute("aria-expanded", "false");
|
|
98
|
+
const slot = document.createElement("slot");
|
|
99
|
+
slot.name = "icon";
|
|
100
|
+
slot.append(icon());
|
|
101
|
+
this.toggle.append(slot);
|
|
102
|
+
this.toggle.addEventListener("click", () => {
|
|
103
|
+
if (this.panel.hidden) this.show();
|
|
104
|
+
else this.hide();
|
|
105
|
+
});
|
|
106
|
+
this.panel = el("div", "panel");
|
|
107
|
+
this.panel.setAttribute("role", "dialog");
|
|
108
|
+
this.panel.hidden = true;
|
|
109
|
+
this.panel.addEventListener("keydown", (event) => {
|
|
110
|
+
if (event.key !== "Escape" || this.hasAttribute("inline")) return;
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
this.hide();
|
|
113
|
+
this.toggle.focus();
|
|
114
|
+
});
|
|
115
|
+
const head = el("div", "head");
|
|
116
|
+
head.setAttribute("role", "tablist");
|
|
117
|
+
this.tabs = {};
|
|
118
|
+
this.pages = {};
|
|
119
|
+
this.body = el("div", "body");
|
|
120
|
+
for (const tab of TABS) {
|
|
121
|
+
const node = button(`tab tab-${tab}`, TAB_NAMES[tab]);
|
|
122
|
+
node.setAttribute("role", "tab");
|
|
123
|
+
node.addEventListener("click", () => {
|
|
124
|
+
this.select(tab);
|
|
125
|
+
});
|
|
126
|
+
this.tabs[tab] = node;
|
|
127
|
+
head.append(node);
|
|
128
|
+
const page = el("div", `page page-${tab}`);
|
|
129
|
+
page.setAttribute("role", "tabpanel");
|
|
130
|
+
page.hidden = true;
|
|
131
|
+
this.pages[tab] = page;
|
|
132
|
+
this.body.append(page);
|
|
133
|
+
}
|
|
134
|
+
this.copy = button("copy");
|
|
135
|
+
this.copy.addEventListener("click", () => {
|
|
136
|
+
this.copyReport();
|
|
137
|
+
});
|
|
138
|
+
const close = button("close", "✕");
|
|
139
|
+
close.setAttribute("aria-label", "Close");
|
|
140
|
+
close.addEventListener("click", () => {
|
|
141
|
+
this.hide();
|
|
142
|
+
this.toggle.focus();
|
|
143
|
+
});
|
|
144
|
+
head.append(this.copy, close);
|
|
145
|
+
const chartTabs = el("div", "chart-tabs");
|
|
146
|
+
chartTabs.setAttribute("role", "tablist");
|
|
147
|
+
this.chartTabs = {};
|
|
148
|
+
for (const tab of CHART_TABS) {
|
|
149
|
+
const node = button(`chart-tab chart-tab-${tab}`, tab);
|
|
150
|
+
node.setAttribute("role", "tab");
|
|
151
|
+
node.addEventListener("click", () => {
|
|
152
|
+
this.chartTab = tab;
|
|
153
|
+
this.markChartTab();
|
|
154
|
+
this.draw();
|
|
155
|
+
});
|
|
156
|
+
this.chartTabs[tab] = node;
|
|
157
|
+
chartTabs.append(node);
|
|
158
|
+
}
|
|
159
|
+
this.windowSelect = el("select", "window");
|
|
160
|
+
this.windowSelect.setAttribute("aria-label", "Window");
|
|
161
|
+
for (const seconds of WINDOWS) {
|
|
162
|
+
const option = document.createElement("option");
|
|
163
|
+
option.value = String(seconds);
|
|
164
|
+
option.textContent = seconds < 60 ? `${seconds} s` : `${seconds / 60} min`;
|
|
165
|
+
this.windowSelect.append(option);
|
|
166
|
+
}
|
|
167
|
+
this.windowSelect.addEventListener("change", () => {
|
|
168
|
+
this.draw();
|
|
169
|
+
});
|
|
170
|
+
chartTabs.append(this.windowSelect);
|
|
171
|
+
this.canvas = el("canvas", "chart");
|
|
172
|
+
const legend = el("div", "legend");
|
|
173
|
+
const readout = el("p", "readout");
|
|
174
|
+
this.pages.charts.append(chartTabs, this.canvas, legend, readout);
|
|
175
|
+
this.charts = createCharts(this.canvas, legend, readout);
|
|
176
|
+
this.outside = (event) => {
|
|
177
|
+
if (!event.composedPath().includes(this)) this.hide();
|
|
178
|
+
};
|
|
179
|
+
this.refit = () => {
|
|
180
|
+
this.fit();
|
|
181
|
+
};
|
|
182
|
+
this.panel.append(head, this.body);
|
|
183
|
+
root.append(style, this.toggle, this.panel);
|
|
184
|
+
this.markTab();
|
|
185
|
+
this.markChartTab();
|
|
186
|
+
this.renderLabels();
|
|
187
|
+
}
|
|
188
|
+
connectedCallback() {
|
|
189
|
+
var _this$parentElement, _this$cancel;
|
|
190
|
+
this.toggleAttribute("inline", ((_this$parentElement = this.parentElement) === null || _this$parentElement === void 0 ? void 0 : _this$parentElement.localName) !== "mbx-control-bar");
|
|
191
|
+
const found = findPlayer(this);
|
|
192
|
+
if (found === null) return;
|
|
193
|
+
(_this$cancel = this.cancel) === null || _this$cancel === void 0 || _this$cancel.call(this);
|
|
194
|
+
this.cancel = whenPlayer(found, (player) => {
|
|
195
|
+
if (!this.isConnected) return;
|
|
196
|
+
this.attach(player);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
disconnectedCallback() {
|
|
200
|
+
var _this$cancel2;
|
|
201
|
+
(_this$cancel2 = this.cancel) === null || _this$cancel2 === void 0 || _this$cancel2.call(this);
|
|
202
|
+
this.cancel = null;
|
|
203
|
+
this.detach();
|
|
204
|
+
}
|
|
205
|
+
attributeChangedCallback() {
|
|
206
|
+
this.renderLabels();
|
|
207
|
+
}
|
|
208
|
+
attach(player) {
|
|
209
|
+
this.player = player;
|
|
210
|
+
this.sampler.attach(player.video, player.engine);
|
|
211
|
+
const on = (target, name, fn) => {
|
|
212
|
+
target.addEventListener(name, fn);
|
|
213
|
+
this.offs.push(() => {
|
|
214
|
+
target.removeEventListener(name, fn);
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
on(player, "sourcechange", () => {
|
|
218
|
+
this.sampler.attach(player.video, player.engine);
|
|
219
|
+
this.poll();
|
|
220
|
+
});
|
|
221
|
+
on(player, "error", (event) => {
|
|
222
|
+
if (event.detail.fatal) this.dispatchEvent(new CustomEvent("report", {
|
|
223
|
+
detail: this.report(),
|
|
224
|
+
bubbles: true,
|
|
225
|
+
composed: true
|
|
226
|
+
}));
|
|
227
|
+
});
|
|
228
|
+
this.timer = setInterval(() => {
|
|
229
|
+
this.poll();
|
|
230
|
+
}, POLL_MS);
|
|
231
|
+
this.poll();
|
|
232
|
+
if (this.hasAttribute("inline")) this.show();
|
|
233
|
+
}
|
|
234
|
+
detach() {
|
|
235
|
+
for (const off of this.offs) off();
|
|
236
|
+
this.offs = [];
|
|
237
|
+
clearInterval(this.timer);
|
|
238
|
+
this.timer = void 0;
|
|
239
|
+
this.sampler.release();
|
|
240
|
+
this.hide();
|
|
241
|
+
this.player = null;
|
|
242
|
+
}
|
|
243
|
+
/** The report, as of now. */
|
|
244
|
+
report() {
|
|
245
|
+
const player = this.player;
|
|
246
|
+
if (player === null) throw new Error("mbx-diagnostics is not inside a player");
|
|
247
|
+
return buildReport({
|
|
248
|
+
player,
|
|
249
|
+
samples: this.sampler.samples,
|
|
250
|
+
marks: this.sampler.marks,
|
|
251
|
+
counters: this.sampler.counters,
|
|
252
|
+
history: this.sampler.history,
|
|
253
|
+
support: this.support
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
poll() {
|
|
257
|
+
const player = this.player;
|
|
258
|
+
if (player === null) return;
|
|
259
|
+
this.sampler.poll(player.video, player.engine);
|
|
260
|
+
if (!this.panel.hidden) this.paint();
|
|
261
|
+
}
|
|
262
|
+
show() {
|
|
263
|
+
if (!this.panel.hidden) return;
|
|
264
|
+
this.panel.hidden = false;
|
|
265
|
+
if (!this.hasAttribute("inline")) {
|
|
266
|
+
this.setAttribute("open", "");
|
|
267
|
+
this.toggle.setAttribute("aria-expanded", "true");
|
|
268
|
+
document.addEventListener("pointerdown", this.outside, true);
|
|
269
|
+
window.addEventListener("resize", this.refit);
|
|
270
|
+
this.fit();
|
|
271
|
+
}
|
|
272
|
+
this.paint();
|
|
273
|
+
this.drawer = setInterval(() => {
|
|
274
|
+
this.draw();
|
|
275
|
+
}, DRAW_MS);
|
|
276
|
+
if (!this.hasAttribute("inline")) this.tabs[this.tab].focus();
|
|
277
|
+
}
|
|
278
|
+
hide() {
|
|
279
|
+
clearInterval(this.drawer);
|
|
280
|
+
this.drawer = void 0;
|
|
281
|
+
if (this.panel.hidden) return;
|
|
282
|
+
this.panel.hidden = true;
|
|
283
|
+
this.removeAttribute("open");
|
|
284
|
+
this.toggle.setAttribute("aria-expanded", "false");
|
|
285
|
+
document.removeEventListener("pointerdown", this.outside, true);
|
|
286
|
+
window.removeEventListener("resize", this.refit);
|
|
287
|
+
}
|
|
288
|
+
/** The panel never leaves the picture: fitted to the room above the button, within the player's width. */
|
|
289
|
+
fit() {
|
|
290
|
+
const player = this.player;
|
|
291
|
+
if (player === null) return;
|
|
292
|
+
const box = player.getBoundingClientRect();
|
|
293
|
+
const host = this.getBoundingClientRect();
|
|
294
|
+
const top = player.video.getBoundingClientRect().top;
|
|
295
|
+
const room = host.top - top - AIR;
|
|
296
|
+
this.panel.style.maxHeight = `${Math.max(120, Math.floor(room))}px`;
|
|
297
|
+
const width = Math.min(PANEL_WIDTH, Math.max(200, box.width - 16));
|
|
298
|
+
this.panel.style.width = `${width}px`;
|
|
299
|
+
const left = Math.max(box.left + AIR, Math.min(host.right - width, box.right - AIR - width));
|
|
300
|
+
this.panel.style.right = "auto";
|
|
301
|
+
this.panel.style.left = `${left - host.left}px`;
|
|
302
|
+
}
|
|
303
|
+
select(tab) {
|
|
304
|
+
this.tab = tab;
|
|
305
|
+
this.markTab();
|
|
306
|
+
this.tabs[tab].focus();
|
|
307
|
+
this.paint();
|
|
308
|
+
this.draw();
|
|
309
|
+
}
|
|
310
|
+
markTab() {
|
|
311
|
+
for (const tab of TABS) {
|
|
312
|
+
this.tabs[tab].setAttribute("aria-selected", String(tab === this.tab));
|
|
313
|
+
this.tabs[tab].tabIndex = tab === this.tab ? 0 : -1;
|
|
314
|
+
this.pages[tab].hidden = tab !== this.tab;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
markChartTab() {
|
|
318
|
+
for (const tab of CHART_TABS) this.chartTabs[tab].setAttribute("aria-selected", String(tab === this.chartTab));
|
|
319
|
+
}
|
|
320
|
+
renderLabels() {
|
|
321
|
+
var _this$getAttribute, _this$getAttribute2;
|
|
322
|
+
this.toggle.setAttribute("aria-label", (_this$getAttribute = this.getAttribute("label")) !== null && _this$getAttribute !== void 0 ? _this$getAttribute : "Diagnostics");
|
|
323
|
+
this.panel.setAttribute("aria-label", (_this$getAttribute2 = this.getAttribute("label")) !== null && _this$getAttribute2 !== void 0 ? _this$getAttribute2 : "Diagnostics");
|
|
324
|
+
if (this.copied === void 0) {
|
|
325
|
+
var _this$getAttribute3;
|
|
326
|
+
this.copy.textContent = (_this$getAttribute3 = this.getAttribute("label-copy")) !== null && _this$getAttribute3 !== void 0 ? _this$getAttribute3 : "Copy report";
|
|
327
|
+
}
|
|
328
|
+
const raw = Number(this.getAttribute("window"));
|
|
329
|
+
const seconds = Number.isFinite(raw) && raw > 0 ? raw : WINDOW;
|
|
330
|
+
if (!WINDOWS.includes(seconds)) {
|
|
331
|
+
const option = document.createElement("option");
|
|
332
|
+
option.value = String(seconds);
|
|
333
|
+
option.textContent = `${seconds} s`;
|
|
334
|
+
this.windowSelect.append(option);
|
|
335
|
+
}
|
|
336
|
+
this.windowSelect.value = String(seconds);
|
|
337
|
+
}
|
|
338
|
+
copyReport() {
|
|
339
|
+
var _this = this;
|
|
340
|
+
return _asyncToGenerator(function* () {
|
|
341
|
+
var _this$getAttribute4;
|
|
342
|
+
const text = JSON.stringify(_this.report(), null, 2);
|
|
343
|
+
try {
|
|
344
|
+
yield navigator.clipboard.writeText(text);
|
|
345
|
+
} catch (_unused) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
_this.copy.textContent = (_this$getAttribute4 = _this.getAttribute("label-copied")) !== null && _this$getAttribute4 !== void 0 ? _this$getAttribute4 : "Copied";
|
|
349
|
+
clearTimeout(_this.copied);
|
|
350
|
+
_this.copied = setTimeout(() => {
|
|
351
|
+
_this.copied = void 0;
|
|
352
|
+
_this.renderLabels();
|
|
353
|
+
}, 1500);
|
|
354
|
+
})();
|
|
355
|
+
}
|
|
356
|
+
/** The colours the charts draw with, off the element's tokens. */
|
|
357
|
+
palette() {
|
|
358
|
+
const computed = getComputedStyle(this);
|
|
359
|
+
const token = (name, fallback) => computed.getPropertyValue(name).trim() || fallback;
|
|
360
|
+
return {
|
|
361
|
+
ink: token("--mbx-text", "#f2f3f5"),
|
|
362
|
+
muted: token("--mbx-muted", "#9aa0a6"),
|
|
363
|
+
accent: token("--mbx-accent", "#5b8cff"),
|
|
364
|
+
warn: token("--mbx-chart-3", "#d9a83f"),
|
|
365
|
+
series: [
|
|
366
|
+
1,
|
|
367
|
+
2,
|
|
368
|
+
3,
|
|
369
|
+
4,
|
|
370
|
+
5
|
|
371
|
+
].map((i) => token(`--mbx-chart-${i}`, "#888"))
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
draw() {
|
|
375
|
+
const player = this.player;
|
|
376
|
+
if (player === null || this.panel.hidden || this.tab !== "charts") return;
|
|
377
|
+
this.charts.draw({
|
|
378
|
+
tab: this.chartTab,
|
|
379
|
+
window: Number(this.windowSelect.value) || WINDOW,
|
|
380
|
+
samples: this.sampler.samples,
|
|
381
|
+
marks: this.sampler.marks,
|
|
382
|
+
counters: this.sampler.counters,
|
|
383
|
+
video: player.video,
|
|
384
|
+
engine: player.engine,
|
|
385
|
+
palette: this.palette()
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/** The page shown, from the facts of the moment. */
|
|
389
|
+
paint() {
|
|
390
|
+
const player = this.player;
|
|
391
|
+
if (player === null || this.panel.hidden) return;
|
|
392
|
+
switch (this.tab) {
|
|
393
|
+
case "playback":
|
|
394
|
+
this.sections(this.pages.playback, this.playbackSections(player));
|
|
395
|
+
break;
|
|
396
|
+
case "engine":
|
|
397
|
+
this.sections(this.pages.engine, this.engineSections(player.engine));
|
|
398
|
+
break;
|
|
399
|
+
case "browser": this.browser();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
playbackSections(player) {
|
|
403
|
+
var _video$getVideoPlayba, _player$getAttribute, _player$getAttribute2, _player$player$sessio, _player$player, _READY_STATES$video$r, _NETWORK_STATES$video, _last$ahead;
|
|
404
|
+
const video = player.video;
|
|
405
|
+
const engine = player.engine;
|
|
406
|
+
const quality = (_video$getVideoPlayba = video.getVideoPlaybackQuality) === null || _video$getVideoPlayba === void 0 ? void 0 : _video$getVideoPlayba.call(video);
|
|
407
|
+
const counters = this.sampler.counters;
|
|
408
|
+
const last = this.sampler.samples[this.sampler.samples.length - 1];
|
|
409
|
+
const state = video.paused ? video.ended ? "ended" : "paused" : "playing";
|
|
410
|
+
const source = [
|
|
411
|
+
["source", (_player$getAttribute = player.getAttribute("src")) !== null && _player$getAttribute !== void 0 ? _player$getAttribute : "–"],
|
|
412
|
+
["type", (_player$getAttribute2 = player.getAttribute("type")) !== null && _player$getAttribute2 !== void 0 ? _player$getAttribute2 : "–"],
|
|
413
|
+
["handler", (_player$player$sessio = (_player$player = player.player) === null || _player$player === void 0 || (_player$player = _player$player.session) === null || _player$player === void 0 ? void 0 : _player$player.handler) !== null && _player$player$sessio !== void 0 ? _player$player$sessio : "–"]
|
|
414
|
+
];
|
|
415
|
+
if (engine !== null) source.push(["phase", engine.stats.snapshot().lifecycle.phase]);
|
|
416
|
+
if (player.error !== null) source.push(["error", `${player.error.category}: ${player.error.code}`]);
|
|
417
|
+
else if (video.error !== null) source.push(["media error", `${video.error.code} ${video.error.message}`]);
|
|
418
|
+
const playback = [
|
|
419
|
+
["time", `${clock(video.currentTime)} of ${clock(video.duration)}`],
|
|
420
|
+
["state", `${state}, ${(_READY_STATES$video$r = READY_STATES[video.readyState]) !== null && _READY_STATES$video$r !== void 0 ? _READY_STATES$video$r : video.readyState}, network ${(_NETWORK_STATES$video = NETWORK_STATES[video.networkState]) !== null && _NETWORK_STATES$video !== void 0 ? _NETWORK_STATES$video : video.networkState}`],
|
|
421
|
+
["rate", `${video.playbackRate}×`],
|
|
422
|
+
["picture", video.videoWidth > 0 ? `${video.videoWidth}×${video.videoHeight}` : "–"],
|
|
423
|
+
["frames", quality === void 0 ? "–" : `${quality.totalVideoFrames} decoded, ${quality.droppedVideoFrames} dropped (${share(quality.droppedVideoFrames, quality.totalVideoFrames)})`],
|
|
424
|
+
["buffered", `${fixed((_last$ahead = last === null || last === void 0 ? void 0 : last.ahead) !== null && _last$ahead !== void 0 ? _last$ahead : 0)}s ahead: ${ranges(playbackRanges(video))}`],
|
|
425
|
+
["stalls", `${counters.stalls}, ${counters.stalledSeconds.toFixed(1)}s in total`]
|
|
426
|
+
];
|
|
427
|
+
if (engine !== null) {
|
|
428
|
+
const snapshot = engine.stats.snapshot();
|
|
429
|
+
playback.push(["throughput", `slow ${bitrate(snapshot.stats.throughputEwma)}, fast ${bitrate(snapshot.stats.throughputFastEwma)}`], ["playing", rendition(engine.quality.playing)], ["switches", String(counters.switches)]);
|
|
430
|
+
}
|
|
431
|
+
return [{
|
|
432
|
+
title: "Source",
|
|
433
|
+
rows: source
|
|
434
|
+
}, {
|
|
435
|
+
title: "Playback",
|
|
436
|
+
rows: playback
|
|
437
|
+
}];
|
|
438
|
+
}
|
|
439
|
+
engineSections(engine) {
|
|
440
|
+
var _optional$live, _optional$live2, _engine$quality$playi, _engine$quality$playi2, _engine$quality$activ, _engine$quality$activ2, _optional$drm$keySyst;
|
|
441
|
+
if (engine === null) return [{
|
|
442
|
+
title: null,
|
|
443
|
+
rows: [],
|
|
444
|
+
note: "A native session: the browser plays this, and there is no engine to ask."
|
|
445
|
+
}];
|
|
446
|
+
const state = engine.stats.snapshot();
|
|
447
|
+
const optional = engine;
|
|
448
|
+
const live = state.live;
|
|
449
|
+
const rows = [
|
|
450
|
+
["capabilities", [...engine.capabilities()].join(", ") || "none"],
|
|
451
|
+
["phase", state.lifecycle.phase],
|
|
452
|
+
["buffer goal", `${state.scheduling.bufferGoal}s`],
|
|
453
|
+
["throughput", `slow ${bitrate(state.stats.throughputEwma)}, fast ${bitrate(state.stats.throughputFastEwma)}`],
|
|
454
|
+
["in flight", [...state.scheduling.inflight.values()].map((r) => `${r.trackId} #${r.seq}`).join(", ") || "none"],
|
|
455
|
+
["live", live === null ? "VOD" : `window ${clock(live.span.start)} to ${clock(live.span.end)}, edge ${clock(live.edge)}${((_optional$live = optional.live) === null || _optional$live === void 0 ? void 0 : _optional$live.latency) === null || ((_optional$live2 = optional.live) === null || _optional$live2 === void 0 ? void 0 : _optional$live2.latency) === void 0 ? "" : `, ${optional.live.latency.toFixed(1)}s behind`}`],
|
|
456
|
+
["trace", `${engine.stats.trace().length} entries`]
|
|
457
|
+
];
|
|
458
|
+
const error = engine.error;
|
|
459
|
+
if (error !== null) rows.push(["last error", `${error.category}: ${error.code}`]);
|
|
460
|
+
const buffers = [...state.buffers.entries()].map(([id, buffer]) => [id.replace("sb:", ""), `${buffer.codecs}: ${ranges(buffer.ranges)}${buffer.pendingAppends > 0 ? `, ${buffer.pendingAppends} pending` : ""}`]);
|
|
461
|
+
const tracks = engine.tracks.available.map((track) => {
|
|
462
|
+
var _engine$tracks$active;
|
|
463
|
+
const active = ((_engine$tracks$active = engine.tracks.active(track.contentType)) === null || _engine$tracks$active === void 0 ? void 0 : _engine$tracks$active.id) === track.id;
|
|
464
|
+
const name = [track.lang, track.role].filter((v) => v !== void 0).join(" · ");
|
|
465
|
+
return [`${track.contentType}${active ? " ●" : ""}`, `${track.id}${name === "" ? "" : ` (${name})`}, ${track.renditions.length} renditions`];
|
|
466
|
+
});
|
|
467
|
+
const playing = (_engine$quality$playi = (_engine$quality$playi2 = engine.quality.playing) === null || _engine$quality$playi2 === void 0 ? void 0 : _engine$quality$playi2.id) !== null && _engine$quality$playi !== void 0 ? _engine$quality$playi : null;
|
|
468
|
+
const active = (_engine$quality$activ = (_engine$quality$activ2 = engine.quality.active) === null || _engine$quality$activ2 === void 0 ? void 0 : _engine$quality$activ2.id) !== null && _engine$quality$activ !== void 0 ? _engine$quality$activ : null;
|
|
469
|
+
const allowed = new Set(engine.quality.allowed.map((r) => r.id));
|
|
470
|
+
const renditions = engine.quality.renditions.map((r) => {
|
|
471
|
+
var _r$codecs;
|
|
472
|
+
const flags = [
|
|
473
|
+
r.id === playing ? "playing" : "",
|
|
474
|
+
r.id === active && active !== playing ? "next" : "",
|
|
475
|
+
r.id === engine.quality.pinned ? "pinned" : "",
|
|
476
|
+
allowed.has(r.id) ? "" : "capped"
|
|
477
|
+
].filter((f) => f !== "");
|
|
478
|
+
const text = [
|
|
479
|
+
r.width !== void 0 && r.height !== void 0 ? `${r.width}×${r.height}` : "",
|
|
480
|
+
bitrate(r.bitrate),
|
|
481
|
+
r.frameRate === void 0 ? "" : `${r.frameRate} fps`,
|
|
482
|
+
(_r$codecs = r.codecs) !== null && _r$codecs !== void 0 ? _r$codecs : ""
|
|
483
|
+
].filter((v) => v !== "").join(", ");
|
|
484
|
+
return [r.id, `${text}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}`];
|
|
485
|
+
});
|
|
486
|
+
const drm = optional.drm === void 0 ? [] : [["key system", (_optional$drm$keySyst = optional.drm.keySystem) !== null && _optional$drm$keySyst !== void 0 ? _optional$drm$keySyst : "none yet"], ["keys", optional.drm.sessions.length === 0 ? "none yet" : optional.drm.sessions.map((s) => `${s.keyId}: ${s.status}`).join(", ")]];
|
|
487
|
+
return [
|
|
488
|
+
{
|
|
489
|
+
title: "Engine",
|
|
490
|
+
rows
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
title: "Source buffers",
|
|
494
|
+
rows: buffers,
|
|
495
|
+
note: buffers.length === 0 ? "none yet" : void 0
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
title: "Tracks",
|
|
499
|
+
rows: tracks,
|
|
500
|
+
note: tracks.length === 0 ? "none yet" : void 0
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
title: "Renditions",
|
|
504
|
+
rows: renditions,
|
|
505
|
+
note: renditions.length === 0 ? "none yet" : void 0
|
|
506
|
+
},
|
|
507
|
+
...optional.drm === void 0 ? [] : [{
|
|
508
|
+
title: "DRM",
|
|
509
|
+
rows: drm
|
|
510
|
+
}]
|
|
511
|
+
];
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Draws sections into a page, reusing what is there: a heading and a
|
|
515
|
+
* list per section, a row per pair, texts replaced only where they
|
|
516
|
+
* changed, so a page open for an hour neither flickers nor grows.
|
|
517
|
+
*/
|
|
518
|
+
sections(page, sections) {
|
|
519
|
+
var _page$lastElementChil;
|
|
520
|
+
const wanted = sections.flatMap((section) => {
|
|
521
|
+
const nodes = [];
|
|
522
|
+
if (section.title !== null) nodes.push(["heading", section.title]);
|
|
523
|
+
if (section.note !== void 0 && section.rows.length === 0) nodes.push(["note", section.note]);
|
|
524
|
+
else nodes.push(["rows", section.rows]);
|
|
525
|
+
return nodes;
|
|
526
|
+
});
|
|
527
|
+
while (page.childElementCount > wanted.length) (_page$lastElementChil = page.lastElementChild) === null || _page$lastElementChil === void 0 || _page$lastElementChil.remove();
|
|
528
|
+
wanted.forEach(([kind, content], i) => {
|
|
529
|
+
var _node$lastElementChil;
|
|
530
|
+
let node = page.children[i];
|
|
531
|
+
const tag = kind === "heading" ? "h3" : kind === "note" ? "p" : "dl";
|
|
532
|
+
if (node === void 0 || node.localName !== tag) {
|
|
533
|
+
const fresh = el(tag, kind);
|
|
534
|
+
if (node === void 0) page.append(fresh);
|
|
535
|
+
else node.replaceWith(fresh);
|
|
536
|
+
node = fresh;
|
|
537
|
+
}
|
|
538
|
+
if (typeof content === "string") {
|
|
539
|
+
if (node.textContent !== content) node.textContent = content;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
while (node.childElementCount > content.length * 2) (_node$lastElementChil = node.lastElementChild) === null || _node$lastElementChil === void 0 || _node$lastElementChil.remove();
|
|
543
|
+
content.forEach(([key, value], j) => {
|
|
544
|
+
let dt = node.children[j * 2];
|
|
545
|
+
let dd = node.children[j * 2 + 1];
|
|
546
|
+
if (dt === void 0 || dd === void 0) {
|
|
547
|
+
dt = el("dt", "key");
|
|
548
|
+
dd = el("dd", "value");
|
|
549
|
+
node.append(dt, dd);
|
|
550
|
+
}
|
|
551
|
+
if (dt.textContent !== key) dt.textContent = key;
|
|
552
|
+
if (dd.textContent !== value) dd.textContent = value;
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
/** The browser page: probed once, the first time it is shown, then a set of tables. */
|
|
557
|
+
browser() {
|
|
558
|
+
const page = this.pages.browser;
|
|
559
|
+
if (this.support !== null || this.probing) return;
|
|
560
|
+
this.probing = true;
|
|
561
|
+
page.replaceChildren(el("p", "note", "Probing the browser…"));
|
|
562
|
+
probeSupport().then((support) => {
|
|
563
|
+
this.support = support;
|
|
564
|
+
this.probing = false;
|
|
565
|
+
this.tables(page, support);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
tables(page, support) {
|
|
569
|
+
page.replaceChildren();
|
|
570
|
+
page.append(el("h3", "heading", "Platform"), el("p", "note", support.userAgent));
|
|
571
|
+
const platform = el("table", "table");
|
|
572
|
+
for (const row of support.platform) {
|
|
573
|
+
const tr = document.createElement("tr");
|
|
574
|
+
const [text, part] = cell(row.value);
|
|
575
|
+
tr.append(el("th", "cell label", row.label), el("td", part, text));
|
|
576
|
+
platform.append(tr);
|
|
577
|
+
}
|
|
578
|
+
page.append(platform);
|
|
579
|
+
page.append(el("h3", "heading", "Codecs"));
|
|
580
|
+
const codecs = el("table", "table");
|
|
581
|
+
const head = document.createElement("tr");
|
|
582
|
+
for (const name of [
|
|
583
|
+
"Codec",
|
|
584
|
+
"MSE fMP4",
|
|
585
|
+
"MSE WebM",
|
|
586
|
+
"<video>",
|
|
587
|
+
"Smooth",
|
|
588
|
+
"Efficient"
|
|
589
|
+
]) head.append(el("th", "cell label", name));
|
|
590
|
+
codecs.append(head);
|
|
591
|
+
for (const row of support.codecs) {
|
|
592
|
+
const tr = document.createElement("tr");
|
|
593
|
+
const label = el("th", "cell label", row.label);
|
|
594
|
+
label.title = row.codec;
|
|
595
|
+
tr.append(label);
|
|
596
|
+
for (const value of [
|
|
597
|
+
row.mse,
|
|
598
|
+
row.webm,
|
|
599
|
+
row.element,
|
|
600
|
+
row.smooth,
|
|
601
|
+
row.efficient
|
|
602
|
+
]) {
|
|
603
|
+
const [text, part] = cell(value);
|
|
604
|
+
tr.append(el("td", part, text));
|
|
605
|
+
}
|
|
606
|
+
codecs.append(tr);
|
|
607
|
+
}
|
|
608
|
+
page.append(codecs);
|
|
609
|
+
page.append(el("h3", "heading", "DRM"));
|
|
610
|
+
const drm = el("table", "table");
|
|
611
|
+
const drmHead = document.createElement("tr");
|
|
612
|
+
for (const name of [
|
|
613
|
+
"System",
|
|
614
|
+
"Available",
|
|
615
|
+
"Level",
|
|
616
|
+
"Schemes",
|
|
617
|
+
"Persistent",
|
|
618
|
+
"Identifier",
|
|
619
|
+
"HDCP"
|
|
620
|
+
]) drmHead.append(el("th", "cell label", name));
|
|
621
|
+
drm.append(drmHead);
|
|
622
|
+
for (const row of support.drm) {
|
|
623
|
+
const tr = document.createElement("tr");
|
|
624
|
+
const label = el("th", "cell label", row.label);
|
|
625
|
+
if (row.keySystem !== null) label.title = row.keySystem;
|
|
626
|
+
tr.append(label);
|
|
627
|
+
const values = [
|
|
628
|
+
row.keySystem === null ? "no" : "yes",
|
|
629
|
+
row.level,
|
|
630
|
+
row.schemes,
|
|
631
|
+
row.persistent,
|
|
632
|
+
row.identifier,
|
|
633
|
+
row.hdcp
|
|
634
|
+
];
|
|
635
|
+
for (const value of values) {
|
|
636
|
+
const [text, part] = cell(value);
|
|
637
|
+
tr.append(el("td", part, text));
|
|
638
|
+
}
|
|
639
|
+
drm.append(tr);
|
|
640
|
+
}
|
|
641
|
+
page.append(drm);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
function playbackRanges(video) {
|
|
645
|
+
return Array.from({ length: video.buffered.length }, (_, i) => ({
|
|
646
|
+
start: video.buffered.start(i),
|
|
647
|
+
end: video.buffered.end(i)
|
|
648
|
+
}));
|
|
649
|
+
}
|
|
650
|
+
//#endregion
|
|
651
|
+
export { MbxDiagnostics };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/format.ts
|
|
2
|
+
/** The numbers as the panel and the charts print them. Pure. */
|
|
3
|
+
/** Bits per second as "3.2 Mbps", "640 kbps" or "0 bps". */
|
|
4
|
+
function bitrate(bps) {
|
|
5
|
+
if (!Number.isFinite(bps) || bps <= 0) return "0 bps";
|
|
6
|
+
if (bps >= 1e6) return `${(bps / 1e6).toFixed(bps >= 1e7 ? 0 : 1)} Mbps`;
|
|
7
|
+
if (bps >= 1e3) return `${Math.round(bps / 1e3)} kbps`;
|
|
8
|
+
return `${Math.round(bps)} bps`;
|
|
9
|
+
}
|
|
10
|
+
/** Seconds as "m:ss", or "h:mm:ss" from an hour up. Anything not finite reads as "–". */
|
|
11
|
+
function clock(seconds) {
|
|
12
|
+
if (!Number.isFinite(seconds)) return "–";
|
|
13
|
+
const total = Math.max(0, Math.floor(seconds));
|
|
14
|
+
const hours = Math.floor(total / 3600);
|
|
15
|
+
const minutes = Math.floor(total % 3600 / 60);
|
|
16
|
+
const rest = total % 60;
|
|
17
|
+
const pad = (n) => n < 10 ? `0${n}` : `${n}`;
|
|
18
|
+
return hours > 0 ? `${hours}:${pad(minutes)}:${pad(rest)}` : `${minutes}:${pad(rest)}`;
|
|
19
|
+
}
|
|
20
|
+
/** A number with `digits` decimals, and "–" when it is not one. */
|
|
21
|
+
function fixed(value, digits = 1) {
|
|
22
|
+
return value === null || value === void 0 || !Number.isFinite(value) ? "–" : value.toFixed(digits);
|
|
23
|
+
}
|
|
24
|
+
/** A share as "1.23%", zero when there is no whole. */
|
|
25
|
+
function share(part, whole) {
|
|
26
|
+
return whole > 0 ? `${(part / whole * 100).toFixed(2)}%` : "0%";
|
|
27
|
+
}
|
|
28
|
+
/** Time ranges as "0.0–12.3, 40.0–52.1". */
|
|
29
|
+
function ranges(list) {
|
|
30
|
+
return list.length === 0 ? "none" : list.map((range) => `${range.start.toFixed(1)}–${range.end.toFixed(1)}`).join(", ");
|
|
31
|
+
}
|
|
32
|
+
/** A rendition's name: its height, else its bitrate. */
|
|
33
|
+
function rendition(r) {
|
|
34
|
+
if (r === null) return "–";
|
|
35
|
+
return r.height !== void 0 ? `${r.height}p, ${bitrate(r.bitrate)}` : bitrate(r.bitrate);
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { bitrate, clock, fixed, ranges, rendition, share };
|