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