@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,34 @@
|
|
|
1
|
+
//#region src/host.ts
|
|
2
|
+
const PLAYER = "mattebox-player";
|
|
3
|
+
/** The parent, or the host of the shadow root `node` sits in, or null at the document. */
|
|
4
|
+
function above(node) {
|
|
5
|
+
const parent = node.parentNode;
|
|
6
|
+
if (parent !== null) return parent;
|
|
7
|
+
return node instanceof ShadowRoot ? node.host : null;
|
|
8
|
+
}
|
|
9
|
+
/** The nearest player above `node`, or null outside one. It may not be upgraded yet. */
|
|
10
|
+
function findPlayer(node) {
|
|
11
|
+
let current = node;
|
|
12
|
+
while (current !== null) {
|
|
13
|
+
if (current instanceof HTMLElement && current.localName === "mattebox-player") return current;
|
|
14
|
+
current = above(current);
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
/** Runs `fn` with the player once it is defined and upgraded, unless cancelled before. */
|
|
19
|
+
function whenPlayer(found, fn) {
|
|
20
|
+
let waiting = true;
|
|
21
|
+
const ready = () => {
|
|
22
|
+
if (!waiting) return;
|
|
23
|
+
waiting = false;
|
|
24
|
+
customElements.upgrade(found);
|
|
25
|
+
if ("video" in found) fn(found);
|
|
26
|
+
};
|
|
27
|
+
if ("video" in found) ready();
|
|
28
|
+
else customElements.whenDefined(PLAYER).then(ready);
|
|
29
|
+
return () => {
|
|
30
|
+
waiting = false;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { PLAYER, findPlayer, whenPlayer };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/icon.ts
|
|
2
|
+
/**
|
|
3
|
+
* The glyph, copied from Vidstack's media-icons 0.10.0
|
|
4
|
+
* (https://github.com/vidstack/media-icons), the same set the player draws
|
|
5
|
+
* its own from: filled paths on a 32-unit viewBox. Copied, not depended
|
|
6
|
+
* on; the notice ships as NOTICE in the package.
|
|
7
|
+
*
|
|
8
|
+
* MIT License
|
|
9
|
+
*
|
|
10
|
+
* Copyright (c) 2023 Rahim Alwer
|
|
11
|
+
*
|
|
12
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
* in the Software without restriction, including without limitation the rights
|
|
15
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
* furnished to do so, subject to the following conditions:
|
|
18
|
+
*
|
|
19
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
* copies or substantial portions of the Software.
|
|
21
|
+
*
|
|
22
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
* SOFTWARE.
|
|
29
|
+
*/
|
|
30
|
+
/** The "info" glyph: a ring with a dot and a bar, drawn even-odd so the ring is hollow. */
|
|
31
|
+
const INFO = "M17.534 10.6674C17.534 11.5143 16.8475 12.2008 16.0006 12.2008C15.1538 12.2008 14.4673 11.5143 14.4673 10.6674C14.4673 9.82061 15.1538 9.13411 16.0006 9.13411C16.8475 9.13411 17.534 9.82061 17.534 10.6674Z M14.6678 14.2005C14.6678 13.8323 14.9663 13.5339 15.3345 13.5339H16.6678C17.036 13.5339 17.3345 13.8323 17.3345 14.2005V22.2005C17.3345 22.5687 17.036 22.8672 16.6678 22.8672H15.3345C14.9663 22.8672 14.6678 22.5687 14.6678 22.2005V14.2005Z M28 16C28 22.6274 22.6274 28 16 28C9.37258 28 4 22.6274 4 16C4 9.37258 9.37258 4 16 4C22.6274 4 28 9.37258 28 16ZM24.9333 16C24.9333 20.9337 20.9337 24.9333 16 24.9333C11.0663 24.9333 7.06667 20.9337 7.06667 16C7.06667 11.0663 11.0663 7.06667 16 7.06667C20.9337 7.06667 24.9333 11.0663 24.9333 16Z";
|
|
32
|
+
const SVG = "http://www.w3.org/2000/svg";
|
|
33
|
+
/** The glyph, hidden from the accessibility tree: the button carries the name. */
|
|
34
|
+
function icon() {
|
|
35
|
+
const svg = document.createElementNS(SVG, "svg");
|
|
36
|
+
svg.setAttribute("viewBox", "0 0 32 32");
|
|
37
|
+
svg.setAttribute("aria-hidden", "true");
|
|
38
|
+
svg.setAttribute("part", "icon");
|
|
39
|
+
const path = document.createElementNS(SVG, "path");
|
|
40
|
+
path.setAttribute("fill-rule", "evenodd");
|
|
41
|
+
path.setAttribute("d", INFO);
|
|
42
|
+
svg.append(path);
|
|
43
|
+
return svg;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { icon };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { probeSupport } from "./support.js";
|
|
2
|
+
import { MbxDiagnostics } from "./element.js";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* @mattebox/player-diagnostics: <mbx-diagnostics>, a control for the
|
|
6
|
+
* player's bar, or a panel under the video, that shows what the session
|
|
7
|
+
* is doing and what the browser can do, and writes a report. Importing
|
|
8
|
+
* this module registers the element; that is the package's one side
|
|
9
|
+
* effect. It reaches the player only through the contract a control of
|
|
10
|
+
* the page's own has, and the engine through its public surface.
|
|
11
|
+
*/
|
|
12
|
+
const DIAGNOSTICS = "mbx-diagnostics";
|
|
13
|
+
if (customElements.get("mbx-diagnostics") === void 0) customElements.define(DIAGNOSTICS, MbxDiagnostics);
|
|
14
|
+
//#endregion
|
|
15
|
+
export { DIAGNOSTICS, MbxDiagnostics, probeSupport };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
//#region src/report.ts
|
|
2
|
+
function ranges(list) {
|
|
3
|
+
return Array.from({ length: list.length }, (_, i) => ({
|
|
4
|
+
start: list.start(i),
|
|
5
|
+
end: list.end(i)
|
|
6
|
+
}));
|
|
7
|
+
}
|
|
8
|
+
function playbackFacts(video) {
|
|
9
|
+
var _video$getVideoPlayba;
|
|
10
|
+
const quality = (_video$getVideoPlayba = video.getVideoPlaybackQuality) === null || _video$getVideoPlayba === void 0 ? void 0 : _video$getVideoPlayba.call(video);
|
|
11
|
+
return {
|
|
12
|
+
currentTime: video.currentTime,
|
|
13
|
+
duration: video.duration,
|
|
14
|
+
paused: video.paused,
|
|
15
|
+
ended: video.ended,
|
|
16
|
+
seeking: video.seeking,
|
|
17
|
+
readyState: video.readyState,
|
|
18
|
+
networkState: video.networkState,
|
|
19
|
+
playbackRate: video.playbackRate,
|
|
20
|
+
volume: video.volume,
|
|
21
|
+
muted: video.muted,
|
|
22
|
+
videoWidth: video.videoWidth,
|
|
23
|
+
videoHeight: video.videoHeight,
|
|
24
|
+
buffered: ranges(video.buffered),
|
|
25
|
+
seekable: ranges(video.seekable),
|
|
26
|
+
frames: quality === void 0 ? null : {
|
|
27
|
+
decoded: quality.totalVideoFrames,
|
|
28
|
+
dropped: quality.droppedVideoFrames
|
|
29
|
+
},
|
|
30
|
+
mediaError: video.error === null ? null : {
|
|
31
|
+
code: video.error.code,
|
|
32
|
+
message: video.error.message
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function engineFacts(engine) {
|
|
37
|
+
var _optional$live$latenc, _optional$live, _engine$quality$playi, _engine$quality$playi2, _engine$quality$activ, _engine$quality$activ2, _error$context;
|
|
38
|
+
const state = engine.stats.snapshot();
|
|
39
|
+
const optional = engine;
|
|
40
|
+
const error = engine.error;
|
|
41
|
+
return {
|
|
42
|
+
phase: state.lifecycle.phase,
|
|
43
|
+
capabilities: [...engine.capabilities()],
|
|
44
|
+
throughput: {
|
|
45
|
+
slow: state.stats.throughputEwma,
|
|
46
|
+
fast: state.stats.throughputFastEwma
|
|
47
|
+
},
|
|
48
|
+
bufferGoal: state.scheduling.bufferGoal,
|
|
49
|
+
buffers: [...state.buffers.entries()].map(([id, buffer]) => ({
|
|
50
|
+
id,
|
|
51
|
+
codecs: buffer.codecs,
|
|
52
|
+
ranges: buffer.ranges.map((range) => ({
|
|
53
|
+
start: range.start,
|
|
54
|
+
end: range.end
|
|
55
|
+
}))
|
|
56
|
+
})),
|
|
57
|
+
inflight: [...state.scheduling.inflight.values()].map((request) => ({
|
|
58
|
+
trackId: request.trackId,
|
|
59
|
+
seq: request.seq,
|
|
60
|
+
url: request.url
|
|
61
|
+
})),
|
|
62
|
+
live: state.live === null ? null : {
|
|
63
|
+
start: state.live.span.start,
|
|
64
|
+
end: state.live.span.end,
|
|
65
|
+
edge: state.live.edge,
|
|
66
|
+
latency: (_optional$live$latenc = (_optional$live = optional.live) === null || _optional$live === void 0 ? void 0 : _optional$live.latency) !== null && _optional$live$latenc !== void 0 ? _optional$live$latenc : null
|
|
67
|
+
},
|
|
68
|
+
tracks: {
|
|
69
|
+
active: Object.fromEntries([
|
|
70
|
+
"video",
|
|
71
|
+
"audio",
|
|
72
|
+
"text"
|
|
73
|
+
].map((type) => {
|
|
74
|
+
var _engine$tracks$active, _engine$tracks$active2;
|
|
75
|
+
return [type, (_engine$tracks$active = (_engine$tracks$active2 = engine.tracks.active(type)) === null || _engine$tracks$active2 === void 0 ? void 0 : _engine$tracks$active2.id) !== null && _engine$tracks$active !== void 0 ? _engine$tracks$active : null];
|
|
76
|
+
})),
|
|
77
|
+
available: engine.tracks.available.map((track) => {
|
|
78
|
+
var _track$lang, _track$role;
|
|
79
|
+
return {
|
|
80
|
+
id: track.id,
|
|
81
|
+
contentType: track.contentType,
|
|
82
|
+
mimeType: track.mimeType,
|
|
83
|
+
lang: (_track$lang = track.lang) !== null && _track$lang !== void 0 ? _track$lang : null,
|
|
84
|
+
role: (_track$role = track.role) !== null && _track$role !== void 0 ? _track$role : null,
|
|
85
|
+
renditions: track.renditions.length
|
|
86
|
+
};
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
quality: {
|
|
90
|
+
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,
|
|
91
|
+
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,
|
|
92
|
+
pinned: engine.quality.pinned,
|
|
93
|
+
allowed: engine.quality.allowed.map((r) => r.id),
|
|
94
|
+
renditions: engine.quality.renditions.map((r) => {
|
|
95
|
+
var _r$width, _r$height, _r$frameRate;
|
|
96
|
+
return {
|
|
97
|
+
id: r.id,
|
|
98
|
+
bitrate: r.bitrate,
|
|
99
|
+
width: (_r$width = r.width) !== null && _r$width !== void 0 ? _r$width : null,
|
|
100
|
+
height: (_r$height = r.height) !== null && _r$height !== void 0 ? _r$height : null,
|
|
101
|
+
frameRate: (_r$frameRate = r.frameRate) !== null && _r$frameRate !== void 0 ? _r$frameRate : null,
|
|
102
|
+
codecs: r.codecs
|
|
103
|
+
};
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
drm: optional.drm === void 0 ? null : {
|
|
107
|
+
keySystem: optional.drm.keySystem,
|
|
108
|
+
sessions: [...optional.drm.sessions]
|
|
109
|
+
},
|
|
110
|
+
error: error === null ? null : {
|
|
111
|
+
category: error.category,
|
|
112
|
+
code: error.code,
|
|
113
|
+
fatal: error.fatal,
|
|
114
|
+
recoverable: error.recoverable,
|
|
115
|
+
context: (_error$context = error.context) !== null && _error$context !== void 0 ? _error$context : null
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const KEEP_MS = 12e4;
|
|
120
|
+
function buildReport(sources) {
|
|
121
|
+
var _player$player$sessio, _player$player;
|
|
122
|
+
const { player } = sources;
|
|
123
|
+
const engine = player.engine;
|
|
124
|
+
const samples = sources.samples.slice(-240);
|
|
125
|
+
const since = performance.now() - KEEP_MS;
|
|
126
|
+
return {
|
|
127
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
128
|
+
userAgent: navigator.userAgent,
|
|
129
|
+
source: {
|
|
130
|
+
url: player.getAttribute("src"),
|
|
131
|
+
type: player.getAttribute("type"),
|
|
132
|
+
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 : null
|
|
133
|
+
},
|
|
134
|
+
playback: playbackFacts(player.video),
|
|
135
|
+
error: player.error,
|
|
136
|
+
engine: engine === null ? null : engineFacts(engine),
|
|
137
|
+
counters: sources.counters,
|
|
138
|
+
samples,
|
|
139
|
+
marks: sources.marks.filter((mark) => mark.t >= since),
|
|
140
|
+
support: sources.support,
|
|
141
|
+
trace: engine === null ? null : sources.history
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { buildReport, engineFacts, playbackFacts };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { slimEntry } from "./trace.js";
|
|
2
|
+
//#region src/sampler.ts
|
|
3
|
+
/** Twenty minutes at two samples a second. */
|
|
4
|
+
const MAX_SAMPLES = 2400;
|
|
5
|
+
/** Trace entries kept, the engine's own default for a ring. */
|
|
6
|
+
const MAX_HISTORY = 500;
|
|
7
|
+
const RECOVERY = /* @__PURE__ */ new Set([
|
|
8
|
+
"nudge",
|
|
9
|
+
"flush",
|
|
10
|
+
"skip",
|
|
11
|
+
"gap-jump"
|
|
12
|
+
]);
|
|
13
|
+
/** A rendition's name for a mark: its height, else its bitrate. */
|
|
14
|
+
function label(r) {
|
|
15
|
+
return r.height !== void 0 ? `${r.height}p` : `${Math.round(r.bitrate / 1e3)} kbps`;
|
|
16
|
+
}
|
|
17
|
+
function createSampler() {
|
|
18
|
+
let samples = [];
|
|
19
|
+
let marks = [];
|
|
20
|
+
let history = [];
|
|
21
|
+
/** Entries already taken, so the event and the ring never count one twice. */
|
|
22
|
+
let seen = /* @__PURE__ */ new WeakSet();
|
|
23
|
+
let off = null;
|
|
24
|
+
let last = null;
|
|
25
|
+
let lastT = 0;
|
|
26
|
+
let lastQuality = null;
|
|
27
|
+
let lastPlayingId = null;
|
|
28
|
+
let stallStart = null;
|
|
29
|
+
let stalls = 0;
|
|
30
|
+
let stalledTotal = 0;
|
|
31
|
+
let switches = 0;
|
|
32
|
+
let decoded = 0;
|
|
33
|
+
let dropped = 0;
|
|
34
|
+
function push(mark) {
|
|
35
|
+
marks.push(mark);
|
|
36
|
+
if (marks.length > MAX_SAMPLES * 2) marks.splice(0, marks.length - MAX_SAMPLES * 2);
|
|
37
|
+
}
|
|
38
|
+
/** The trace entries since the last read, oldest first. */
|
|
39
|
+
function fresh(engine) {
|
|
40
|
+
const trace = engine.stats.trace();
|
|
41
|
+
let from = 0;
|
|
42
|
+
if (last !== null) {
|
|
43
|
+
const at = trace.indexOf(last);
|
|
44
|
+
from = at >= 0 ? at + 1 : trace.findIndex((entry) => entry.t > lastT);
|
|
45
|
+
if (from < 0) from = trace.length;
|
|
46
|
+
}
|
|
47
|
+
const tail = trace[trace.length - 1];
|
|
48
|
+
if (tail !== void 0) {
|
|
49
|
+
last = tail;
|
|
50
|
+
lastT = tail.t;
|
|
51
|
+
}
|
|
52
|
+
return trace.slice(from);
|
|
53
|
+
}
|
|
54
|
+
/** One entry in: its marks for the charts, and its slimmed self for the history. */
|
|
55
|
+
function take(entry, video) {
|
|
56
|
+
if (seen.has(entry)) return;
|
|
57
|
+
seen.add(entry);
|
|
58
|
+
history.push(slimEntry(entry));
|
|
59
|
+
if (history.length > MAX_HISTORY) history.splice(0, history.length - MAX_HISTORY);
|
|
60
|
+
{
|
|
61
|
+
const msg = entry.msg;
|
|
62
|
+
if (msg.type === "SEGMENT_LOADED" && msg.trackId !== "manifest" && msg.size > 0) push({
|
|
63
|
+
t: entry.t,
|
|
64
|
+
kind: "segment",
|
|
65
|
+
value: msg.size * 8e3 / Math.max(msg.rtt, 1),
|
|
66
|
+
label: `${msg.trackId} #${msg.seq}`,
|
|
67
|
+
trackId: msg.trackId
|
|
68
|
+
});
|
|
69
|
+
else if (msg.type === "STALLED") push({
|
|
70
|
+
t: entry.t,
|
|
71
|
+
kind: "stall",
|
|
72
|
+
value: msg.at,
|
|
73
|
+
label: `stalled at ${msg.at.toFixed(1)}s`
|
|
74
|
+
});
|
|
75
|
+
else if (msg.type === "SEEKING") push({
|
|
76
|
+
t: entry.t,
|
|
77
|
+
kind: "seek",
|
|
78
|
+
value: msg.to,
|
|
79
|
+
label: `seek to ${msg.to.toFixed(1)}s`
|
|
80
|
+
});
|
|
81
|
+
for (const effect of entry.effects) {
|
|
82
|
+
if (effect.kind !== "emit") continue;
|
|
83
|
+
const name = effect.event.replace("recovery:", "");
|
|
84
|
+
if (RECOVERY.has(name)) push({
|
|
85
|
+
t: entry.t,
|
|
86
|
+
kind: name,
|
|
87
|
+
value: video.currentTime,
|
|
88
|
+
label: `recovery ${name}`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function reset() {
|
|
94
|
+
samples = [];
|
|
95
|
+
marks = [];
|
|
96
|
+
history = [];
|
|
97
|
+
seen = /* @__PURE__ */ new WeakSet();
|
|
98
|
+
last = null;
|
|
99
|
+
lastT = 0;
|
|
100
|
+
lastQuality = null;
|
|
101
|
+
lastPlayingId = null;
|
|
102
|
+
stallStart = null;
|
|
103
|
+
stalls = 0;
|
|
104
|
+
stalledTotal = 0;
|
|
105
|
+
switches = 0;
|
|
106
|
+
decoded = 0;
|
|
107
|
+
dropped = 0;
|
|
108
|
+
}
|
|
109
|
+
function release() {
|
|
110
|
+
off === null || off === void 0 || off();
|
|
111
|
+
off = null;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
attach(video, engine) {
|
|
115
|
+
release();
|
|
116
|
+
reset();
|
|
117
|
+
if (engine === null) return;
|
|
118
|
+
off = engine.on("trace", (entry) => {
|
|
119
|
+
take(entry, video);
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
release,
|
|
123
|
+
poll(video, engine) {
|
|
124
|
+
var _engine$stats$snapsho, _engine$quality$playi, _video$getVideoPlayba, _quality$totalVideoFr, _quality$droppedVideo, _state$stats$throughp, _state$stats$throughp2, _playing$id, _playing$bitrate, _playing$height2;
|
|
125
|
+
const now = performance.now();
|
|
126
|
+
if (engine !== null) for (const entry of fresh(engine)) take(entry, video);
|
|
127
|
+
const state = (_engine$stats$snapsho = engine === null || engine === void 0 ? void 0 : engine.stats.snapshot()) !== null && _engine$stats$snapsho !== void 0 ? _engine$stats$snapsho : null;
|
|
128
|
+
const playing = (_engine$quality$playi = engine === null || engine === void 0 ? void 0 : engine.quality.playing) !== null && _engine$quality$playi !== void 0 ? _engine$quality$playi : null;
|
|
129
|
+
const quality = (_video$getVideoPlayba = video.getVideoPlaybackQuality) === null || _video$getVideoPlayba === void 0 ? void 0 : _video$getVideoPlayba.call(video);
|
|
130
|
+
decoded = (_quality$totalVideoFr = quality === null || quality === void 0 ? void 0 : quality.totalVideoFrames) !== null && _quality$totalVideoFr !== void 0 ? _quality$totalVideoFr : 0;
|
|
131
|
+
dropped = (_quality$droppedVideo = quality === null || quality === void 0 ? void 0 : quality.droppedVideoFrames) !== null && _quality$droppedVideo !== void 0 ? _quality$droppedVideo : 0;
|
|
132
|
+
const droppedDelta = lastQuality === null ? 0 : Math.max(0, dropped - lastQuality.dropped);
|
|
133
|
+
const decodedDelta = lastQuality === null ? 0 : Math.max(0, decoded - lastQuality.decoded);
|
|
134
|
+
lastQuality = {
|
|
135
|
+
dropped,
|
|
136
|
+
decoded
|
|
137
|
+
};
|
|
138
|
+
let ahead = 0;
|
|
139
|
+
for (let i = 0; i < video.buffered.length; i += 1) if (video.buffered.start(i) <= video.currentTime + .25 && video.buffered.end(i) > video.currentTime) ahead = video.buffered.end(i) - video.currentTime;
|
|
140
|
+
const stalled = !video.paused && !video.ended && !video.seeking && video.readyState < 3 && video.currentTime > 0;
|
|
141
|
+
if (stalled && stallStart === null) {
|
|
142
|
+
stallStart = now;
|
|
143
|
+
stalls += 1;
|
|
144
|
+
} else if (!stalled && stallStart !== null) {
|
|
145
|
+
stalledTotal += (now - stallStart) / 1e3;
|
|
146
|
+
stallStart = null;
|
|
147
|
+
}
|
|
148
|
+
if (playing !== null && lastPlayingId !== null && playing.id !== lastPlayingId) {
|
|
149
|
+
var _playing$height;
|
|
150
|
+
switches += 1;
|
|
151
|
+
push({
|
|
152
|
+
t: now,
|
|
153
|
+
kind: "switch",
|
|
154
|
+
value: (_playing$height = playing.height) !== null && _playing$height !== void 0 ? _playing$height : playing.bitrate,
|
|
155
|
+
label: `now ${label(playing)}`
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (playing !== null) lastPlayingId = playing.id;
|
|
159
|
+
samples.push({
|
|
160
|
+
t: now,
|
|
161
|
+
currentTime: video.currentTime,
|
|
162
|
+
slow: (_state$stats$throughp = state === null || state === void 0 ? void 0 : state.stats.throughputEwma) !== null && _state$stats$throughp !== void 0 ? _state$stats$throughp : 0,
|
|
163
|
+
fast: (_state$stats$throughp2 = state === null || state === void 0 ? void 0 : state.stats.throughputFastEwma) !== null && _state$stats$throughp2 !== void 0 ? _state$stats$throughp2 : 0,
|
|
164
|
+
ahead,
|
|
165
|
+
stalled,
|
|
166
|
+
dropped: droppedDelta,
|
|
167
|
+
decoded: decodedDelta,
|
|
168
|
+
playingId: (_playing$id = playing === null || playing === void 0 ? void 0 : playing.id) !== null && _playing$id !== void 0 ? _playing$id : null,
|
|
169
|
+
playingBitrate: (_playing$bitrate = playing === null || playing === void 0 ? void 0 : playing.bitrate) !== null && _playing$bitrate !== void 0 ? _playing$bitrate : 0,
|
|
170
|
+
playingHeight: (_playing$height2 = playing === null || playing === void 0 ? void 0 : playing.height) !== null && _playing$height2 !== void 0 ? _playing$height2 : 0
|
|
171
|
+
});
|
|
172
|
+
if (samples.length > MAX_SAMPLES) samples.splice(0, samples.length - MAX_SAMPLES);
|
|
173
|
+
},
|
|
174
|
+
get samples() {
|
|
175
|
+
return samples;
|
|
176
|
+
},
|
|
177
|
+
get marks() {
|
|
178
|
+
return marks;
|
|
179
|
+
},
|
|
180
|
+
get history() {
|
|
181
|
+
return history;
|
|
182
|
+
},
|
|
183
|
+
get counters() {
|
|
184
|
+
const current = stallStart === null ? 0 : (performance.now() - stallStart) / 1e3;
|
|
185
|
+
return {
|
|
186
|
+
stalls,
|
|
187
|
+
stalledSeconds: stalledTotal + current,
|
|
188
|
+
switches,
|
|
189
|
+
decoded,
|
|
190
|
+
dropped
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
export { createSampler };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
//#region src/style.ts
|
|
2
|
+
/**
|
|
3
|
+
* The element's stylesheet. The button is drawn the way the player's own
|
|
4
|
+
* buttons are, in a copy: the player's styles are internal, and a control
|
|
5
|
+
* of the page's own carries its own sheet. Tokens come through the
|
|
6
|
+
* `--mbx-*` custom properties the player sets, which inherit through light
|
|
7
|
+
* DOM; the chart colours have defaults here and a page overrides them the
|
|
8
|
+
* same way. Nothing carries `!important`, so a page's `::part()` rule wins.
|
|
9
|
+
*/
|
|
10
|
+
const STYLE = `
|
|
11
|
+
:host {
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
position: relative;
|
|
14
|
+
--mbx-chart-1: #5aa9e0;
|
|
15
|
+
--mbx-chart-2: #6fbf8f;
|
|
16
|
+
--mbx-chart-3: #d9a83f;
|
|
17
|
+
--mbx-chart-4: #e05a6e;
|
|
18
|
+
--mbx-chart-5: #a98fe0;
|
|
19
|
+
}
|
|
20
|
+
:host([hidden]) { display: none; }
|
|
21
|
+
:host([inline]) { display: block; }
|
|
22
|
+
[part~="button"] {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
flex: none;
|
|
27
|
+
width: 40px;
|
|
28
|
+
height: 40px;
|
|
29
|
+
padding: 0;
|
|
30
|
+
font: inherit;
|
|
31
|
+
color: inherit;
|
|
32
|
+
background: transparent;
|
|
33
|
+
border: 0;
|
|
34
|
+
border-radius: var(--mbx-radius);
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
opacity: 0.9;
|
|
37
|
+
}
|
|
38
|
+
[part~="button"]:hover { opacity: 1; }
|
|
39
|
+
[part~="button"]:focus-visible { outline: 2px solid var(--mbx-accent); outline-offset: -2px; }
|
|
40
|
+
[part~="icon"], ::slotted(*) { width: 1.75em; height: 1.75em; fill: currentColor; }
|
|
41
|
+
:host([inline]) [part~="button"] { display: none; }
|
|
42
|
+
[part~="panel"] {
|
|
43
|
+
position: absolute;
|
|
44
|
+
right: 0;
|
|
45
|
+
bottom: 100%;
|
|
46
|
+
box-sizing: border-box;
|
|
47
|
+
width: 560px;
|
|
48
|
+
margin-bottom: 8px;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
color: var(--mbx-text, #f2f3f5);
|
|
53
|
+
background: rgba(16, 17, 20, 0.95);
|
|
54
|
+
border-radius: var(--mbx-radius, 4px);
|
|
55
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
|
56
|
+
font: 400 12px/1.5 var(--mbx-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
|
57
|
+
text-align: left;
|
|
58
|
+
white-space: normal;
|
|
59
|
+
}
|
|
60
|
+
[part~="panel"][hidden] { display: none; }
|
|
61
|
+
:host([inline]) [part~="panel"] {
|
|
62
|
+
position: static;
|
|
63
|
+
width: auto;
|
|
64
|
+
max-height: none;
|
|
65
|
+
margin: 0;
|
|
66
|
+
box-shadow: none;
|
|
67
|
+
border-radius: 0;
|
|
68
|
+
}
|
|
69
|
+
[part~="head"] {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 4px;
|
|
73
|
+
padding: 6px 8px;
|
|
74
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
|
75
|
+
}
|
|
76
|
+
[part~="tab"], [part~="chart-tab"] {
|
|
77
|
+
font: inherit;
|
|
78
|
+
color: var(--mbx-muted, #9aa0a6);
|
|
79
|
+
background: transparent;
|
|
80
|
+
border: 0;
|
|
81
|
+
border-radius: var(--mbx-radius, 4px);
|
|
82
|
+
padding: 3px 8px;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
[part~="tab"][aria-selected="true"], [part~="chart-tab"][aria-selected="true"] {
|
|
86
|
+
color: var(--mbx-text, #f2f3f5);
|
|
87
|
+
background: rgba(255, 255, 255, 0.12);
|
|
88
|
+
}
|
|
89
|
+
[part~="copy"], [part~="close"], [part~="window"] {
|
|
90
|
+
font: inherit;
|
|
91
|
+
color: inherit;
|
|
92
|
+
background: transparent;
|
|
93
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
94
|
+
border-radius: var(--mbx-radius, 4px);
|
|
95
|
+
padding: 2px 8px;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
[part~="copy"] { margin-left: auto; }
|
|
99
|
+
[part~="window"] { margin-left: auto; }
|
|
100
|
+
[part~="window"] option { color: #000; }
|
|
101
|
+
[part~="tab"]:focus-visible, [part~="chart-tab"]:focus-visible, [part~="copy"]:focus-visible,
|
|
102
|
+
[part~="close"]:focus-visible, [part~="window"]:focus-visible {
|
|
103
|
+
outline: 2px solid var(--mbx-accent, #5b8cff);
|
|
104
|
+
outline-offset: -2px;
|
|
105
|
+
}
|
|
106
|
+
[part~="body"] { min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 8px; }
|
|
107
|
+
[part~="page"][hidden] { display: none; }
|
|
108
|
+
[part~="rows"] { display: grid; grid-template-columns: max-content 1fr; gap: 2px 12px; margin: 0; }
|
|
109
|
+
[part~="key"] { color: var(--mbx-muted, #9aa0a6); white-space: nowrap; }
|
|
110
|
+
[part~="value"] { margin: 0; overflow-wrap: anywhere; }
|
|
111
|
+
[part~="heading"] {
|
|
112
|
+
margin: 10px 0 4px;
|
|
113
|
+
font-size: 11px;
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
letter-spacing: 0.06em;
|
|
116
|
+
text-transform: uppercase;
|
|
117
|
+
color: var(--mbx-muted, #9aa0a6);
|
|
118
|
+
}
|
|
119
|
+
[part~="heading"]:first-child { margin-top: 0; }
|
|
120
|
+
[part~="note"] { margin: 0; color: var(--mbx-muted, #9aa0a6); }
|
|
121
|
+
[part~="chart-tabs"] { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-bottom: 6px; }
|
|
122
|
+
[part~="chart"] { display: block; width: 100%; height: 160px; }
|
|
123
|
+
[part~="legend"] { display: flex; flex-wrap: wrap; gap: 2px 12px; margin-top: 4px; color: var(--mbx-muted, #9aa0a6); }
|
|
124
|
+
[part~="swatch"] { display: inline-block; width: 10px; height: 10px; margin-right: 4px; border-radius: 2px; vertical-align: middle; }
|
|
125
|
+
[part~="readout"] { margin-top: 4px; }
|
|
126
|
+
[part~="table"] { width: 100%; border-collapse: collapse; margin-bottom: 4px; }
|
|
127
|
+
[part~="cell"] { padding: 2px 6px; text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.08); white-space: nowrap; font-weight: 400; }
|
|
128
|
+
[part~="label"] { color: var(--mbx-muted, #9aa0a6); }
|
|
129
|
+
[part~="ok"] { color: var(--mbx-chart-2); }
|
|
130
|
+
[part~="bad"] { color: var(--mbx-chart-4); }
|
|
131
|
+
[part~="na"] { color: var(--mbx-muted, #9aa0a6); }
|
|
132
|
+
[part~="ok"], [part~="bad"], [part~="na"], [part~="maybe"] { font-weight: 600; }
|
|
133
|
+
`;
|
|
134
|
+
//#endregion
|
|
135
|
+
export { STYLE };
|