@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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +24 -0
  3. package/README.md +45 -0
  4. package/dist/cdn/mattebox-player-diagnostics.min.js +124 -0
  5. package/dist/charts.d.ts +35 -0
  6. package/dist/charts.d.ts.map +1 -0
  7. package/dist/charts.js +401 -0
  8. package/dist/charts.js.map +1 -0
  9. package/dist/element.d.ts +62 -0
  10. package/dist/element.d.ts.map +1 -0
  11. package/dist/element.js +677 -0
  12. package/dist/element.js.map +1 -0
  13. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/asyncToGenerator.js +27 -0
  14. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/defineProperty.js +12 -0
  15. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/objectSpread2.js +25 -0
  16. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/toPrimitive.js +14 -0
  17. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/toPropertyKey.js +9 -0
  18. package/dist/es2015/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/typeof.js +11 -0
  19. package/dist/es2015/charts.js +380 -0
  20. package/dist/es2015/element.js +651 -0
  21. package/dist/es2015/format.js +38 -0
  22. package/dist/es2015/host.js +34 -0
  23. package/dist/es2015/icon.js +46 -0
  24. package/dist/es2015/index.js +15 -0
  25. package/dist/es2015/report.js +145 -0
  26. package/dist/es2015/sampler.js +196 -0
  27. package/dist/es2015/style.js +135 -0
  28. package/dist/es2015/support.js +351 -0
  29. package/dist/es2015/trace.js +99 -0
  30. package/dist/format.d.ts +20 -0
  31. package/dist/format.d.ts.map +1 -0
  32. package/dist/format.js +45 -0
  33. package/dist/format.js.map +1 -0
  34. package/dist/host.d.ts +19 -0
  35. package/dist/host.d.ts.map +1 -0
  36. package/dist/host.js +38 -0
  37. package/dist/host.js.map +1 -0
  38. package/dist/icon.d.ts +31 -0
  39. package/dist/icon.d.ts.map +1 -0
  40. package/dist/icon.js +44 -0
  41. package/dist/icon.js.map +1 -0
  42. package/dist/index.d.ts +21 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +16 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/report.d.ts +140 -0
  47. package/dist/report.d.ts.map +1 -0
  48. package/dist/report.js +125 -0
  49. package/dist/report.js.map +1 -0
  50. package/dist/sampler.d.ts +68 -0
  51. package/dist/sampler.d.ts.map +1 -0
  52. package/dist/sampler.js +208 -0
  53. package/dist/sampler.js.map +1 -0
  54. package/dist/style.d.ts +10 -0
  55. package/dist/style.d.ts.map +1 -0
  56. package/dist/style.js +133 -0
  57. package/dist/style.js.map +1 -0
  58. package/dist/support.d.ts +46 -0
  59. package/dist/support.d.ts.map +1 -0
  60. package/dist/support.js +247 -0
  61. package/dist/support.js.map +1 -0
  62. package/dist/trace.d.ts +14 -0
  63. package/dist/trace.d.ts.map +1 -0
  64. package/dist/trace.js +100 -0
  65. package/dist/trace.js.map +1 -0
  66. package/package.json +59 -0
@@ -0,0 +1,677 @@
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';
9
+ const POLL_MS = 500;
10
+ const DRAW_MS = 250;
11
+ const WINDOW = 120;
12
+ const WINDOWS = [30, 60, 120, 300, 600];
13
+ const PANEL_WIDTH = 560;
14
+ /** Air between the panel's top and the picture's, and around it. */
15
+ const AIR = 8;
16
+ const TABS = ['playback', 'charts', 'engine', 'browser'];
17
+ const TAB_NAMES = {
18
+ playback: 'Playback',
19
+ charts: 'Charts',
20
+ engine: 'Engine',
21
+ browser: 'Browser',
22
+ };
23
+ const READY_STATES = ['nothing', 'metadata', 'current data', 'future data', 'enough data'];
24
+ const NETWORK_STATES = ['empty', 'idle', 'loading', 'no source'];
25
+ 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;
31
+ }
32
+ function button(part, text) {
33
+ const node = el('button', part, text);
34
+ node.type = 'button';
35
+ return node;
36
+ }
37
+ /** A cell's text and its part: a tick, a cross, a dash, or the text itself. */
38
+ 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
+ }
670
+ }
671
+ 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
+ }));
676
+ }
677
+ //# sourceMappingURL=element.js.map