@mattebox/player-diagnostics 0.9.0 → 0.10.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/dist/cdn/mattebox-player-diagnostics.min.js +1 -1
- package/dist/charts.js +1 -2
- package/dist/dom.js +1 -2
- package/dist/element.d.ts +13 -2
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +35 -11
- package/dist/element.js.map +1 -1
- package/dist/es2015/_virtual/{_@oxc-project_runtime@0.154.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/asyncToGenerator.js +1 -2
- package/dist/es2015/_virtual/{_@oxc-project_runtime@0.154.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/defineProperty.js +1 -2
- package/dist/es2015/_virtual/{_@oxc-project_runtime@0.154.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/objectSpread2.js +1 -2
- package/dist/es2015/_virtual/{_@oxc-project_runtime@0.154.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/toPrimitive.js +1 -2
- package/dist/es2015/_virtual/{_@oxc-project_runtime@0.154.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/toPropertyKey.js +1 -2
- package/dist/es2015/_virtual/{_@oxc-project_runtime@0.154.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/typeof.js +1 -2
- package/dist/es2015/charts.js +1 -2
- package/dist/es2015/dom.js +1 -2
- package/dist/es2015/element.js +39 -17
- package/dist/es2015/format.js +1 -2
- package/dist/es2015/host.js +1 -2
- package/dist/es2015/icon.js +1 -2
- package/dist/es2015/index.js +1 -2
- package/dist/es2015/locales/ar.js +8 -0
- package/dist/es2015/locales/ca.js +8 -0
- package/dist/es2015/locales/de.js +8 -0
- package/dist/es2015/locales/es.js +8 -0
- package/dist/es2015/locales/fr.js +8 -0
- package/dist/es2015/locales/hi.js +8 -0
- package/dist/es2015/locales/it.js +8 -0
- package/dist/es2015/locales/ja.js +8 -0
- package/dist/es2015/locales/ko.js +8 -0
- package/dist/es2015/locales/pt.js +8 -0
- package/dist/es2015/locales/zh.js +8 -0
- package/dist/es2015/report.js +16 -4
- package/dist/es2015/sampler.js +1 -2
- package/dist/es2015/style.js +0 -1
- package/dist/es2015/support.js +3 -4
- package/dist/es2015/trace.js +2 -3
- package/dist/format.js +1 -2
- package/dist/host.js +1 -2
- package/dist/icon.js +1 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/locales/ar.d.ts +8 -0
- package/dist/locales/ar.d.ts.map +1 -0
- package/dist/locales/ar.js +10 -0
- package/dist/locales/ar.js.map +1 -0
- package/dist/locales/ca.d.ts +8 -0
- package/dist/locales/ca.d.ts.map +1 -0
- package/dist/locales/ca.js +10 -0
- package/dist/locales/ca.js.map +1 -0
- package/dist/locales/de.d.ts +8 -0
- package/dist/locales/de.d.ts.map +1 -0
- package/dist/locales/de.js +10 -0
- package/dist/locales/de.js.map +1 -0
- package/dist/locales/es.d.ts +8 -0
- package/dist/locales/es.d.ts.map +1 -0
- package/dist/locales/es.js +10 -0
- package/dist/locales/es.js.map +1 -0
- package/dist/locales/fr.d.ts +8 -0
- package/dist/locales/fr.d.ts.map +1 -0
- package/dist/locales/fr.js +10 -0
- package/dist/locales/fr.js.map +1 -0
- package/dist/locales/hi.d.ts +8 -0
- package/dist/locales/hi.d.ts.map +1 -0
- package/dist/locales/hi.js +10 -0
- package/dist/locales/hi.js.map +1 -0
- package/dist/locales/it.d.ts +8 -0
- package/dist/locales/it.d.ts.map +1 -0
- package/dist/locales/it.js +10 -0
- package/dist/locales/it.js.map +1 -0
- package/dist/locales/ja.d.ts +8 -0
- package/dist/locales/ja.d.ts.map +1 -0
- package/dist/locales/ja.js +10 -0
- package/dist/locales/ja.js.map +1 -0
- package/dist/locales/ko.d.ts +8 -0
- package/dist/locales/ko.d.ts.map +1 -0
- package/dist/locales/ko.js +10 -0
- package/dist/locales/ko.js.map +1 -0
- package/dist/locales/pt.d.ts +8 -0
- package/dist/locales/pt.d.ts.map +1 -0
- package/dist/locales/pt.js +10 -0
- package/dist/locales/pt.js.map +1 -0
- package/dist/locales/zh.d.ts +8 -0
- package/dist/locales/zh.d.ts.map +1 -0
- package/dist/locales/zh.js +10 -0
- package/dist/locales/zh.js.map +1 -0
- package/dist/messages.d.ts +22 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/report.d.ts +9 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +12 -4
- package/dist/report.js.map +1 -1
- package/dist/sampler.js +1 -2
- package/dist/style.js +0 -1
- package/dist/support.js +1 -2
- package/dist/trace.js +1 -2
- package/package.json +9 -4
package/dist/report.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// src/report.ts
|
|
2
|
+
/** The trackers of `player`, or none on a player older than trackers. */
|
|
3
|
+
function trackersOf(player) {
|
|
4
|
+
return (player.trackers?.list ?? []).map((tracker) => ({
|
|
5
|
+
name: tracker.name,
|
|
6
|
+
session: tracker.session ?? null,
|
|
7
|
+
device: tracker.device ?? null
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
2
10
|
function ranges(list) {
|
|
3
11
|
return Array.from({ length: list.length }, (_, i) => ({
|
|
4
12
|
start: list.start(i),
|
|
@@ -132,10 +140,10 @@ function buildReport(sources) {
|
|
|
132
140
|
samples,
|
|
133
141
|
marks: sources.marks.filter((mark) => mark.t >= since),
|
|
134
142
|
support: sources.support,
|
|
135
|
-
trace: engine === null ? null : sources.history
|
|
143
|
+
trace: engine === null ? null : sources.history,
|
|
144
|
+
trackers: trackersOf(player)
|
|
136
145
|
};
|
|
137
146
|
}
|
|
138
|
-
|
|
139
|
-
export { buildReport, engineFacts, playbackFacts };
|
|
147
|
+
export { buildReport, engineFacts, playbackFacts, trackersOf };
|
|
140
148
|
|
|
141
149
|
//# sourceMappingURL=report.js.map
|
package/dist/report.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.js","names":[],"sources":["../src/report.ts"],"sourcesContent":["/**\n * The report: everything the panel shows, as one plain object a page can\n * send when something goes wrong, and the trace the engine's guide asks\n * for with every error report (chapter 12), as the element kept it: see\n * `trace.ts` for what is kept of each entry. Every field is serializable;\n * a native session has no engine part.\n */\nimport type { Mattebox } from 'mattebox';\nimport type { PlayerError, PlayerHost } from './host.js';\nimport type { Counters, Mark, Sample } from './sampler.js';\nimport type { Support } from './support.js';\n\nexport interface RangeReport {\n readonly start: number;\n readonly end: number;\n}\n\nexport interface PlaybackReport {\n readonly currentTime: number;\n readonly duration: number;\n readonly paused: boolean;\n readonly ended: boolean;\n readonly seeking: boolean;\n readonly readyState: number;\n readonly networkState: number;\n readonly playbackRate: number;\n readonly volume: number;\n readonly muted: boolean;\n readonly videoWidth: number;\n readonly videoHeight: number;\n readonly buffered: readonly RangeReport[];\n readonly seekable: readonly RangeReport[];\n readonly frames: { readonly decoded: number; readonly dropped: number } | null;\n readonly mediaError: { readonly code: number; readonly message: string } | null;\n}\n\nexport interface EngineReport {\n readonly phase: string;\n readonly capabilities: readonly string[];\n readonly throughput: { readonly slow: number; readonly fast: number };\n readonly bufferGoal: number;\n readonly buffers: ReadonlyArray<{\n readonly id: string;\n readonly codecs: string;\n readonly ranges: readonly RangeReport[];\n }>;\n readonly inflight: ReadonlyArray<{\n readonly trackId: string;\n readonly seq: number;\n readonly url: string;\n }>;\n readonly live: {\n readonly start: number;\n readonly end: number;\n readonly edge: number;\n readonly latency: number | null;\n } | null;\n readonly tracks: {\n readonly active: Readonly<Record<string, string | null>>;\n readonly available: ReadonlyArray<{\n readonly id: string;\n readonly contentType: string;\n readonly mimeType: string;\n readonly lang: string | null;\n readonly role: string | null;\n readonly renditions: number;\n }>;\n };\n readonly quality: {\n readonly playing: string | null;\n readonly active: string | null;\n readonly pinned: string | null;\n readonly allowed: readonly string[];\n readonly renditions: ReadonlyArray<{\n readonly id: string;\n readonly bitrate: number;\n readonly width: number | null;\n readonly height: number | null;\n readonly frameRate: number | null;\n readonly codecs: string | null;\n }>;\n };\n readonly drm: {\n readonly keySystem: string | null;\n readonly sessions: ReadonlyArray<{ readonly keyId: string; readonly status: string }>;\n } | null;\n /** The engine's qoe stage figures for this load, or null without the stage. */\n readonly qoe: QoeFigures | null;\n readonly error: {\n readonly category: string;\n readonly code: string;\n readonly fatal: boolean;\n readonly recoverable: boolean;\n readonly context: Readonly<Record<string, unknown>> | null;\n } | null;\n}\n\nexport interface DiagnosticsReport {\n /** When the report was taken, ISO 8601. */\n readonly at: string;\n readonly userAgent: string;\n readonly source: {\n readonly url: string | null;\n readonly type: string | null;\n /** The name of the handler that won, or null before a session. */\n readonly handler: string | null;\n };\n readonly playback: PlaybackReport;\n /** The player's fatal error of the current load, or null. */\n readonly error: PlayerError | null;\n /** Null for a native session. */\n readonly engine: EngineReport | null;\n readonly counters: Counters;\n /** The last two minutes of samples, and the marks in them. */\n readonly samples: readonly Sample[];\n readonly marks: readonly Mark[];\n /** What the browser supports, when it was probed before the report. */\n readonly support: Support | null;\n /** The trace entries the element kept, slimmed, oldest first, or null for a native session. */\n readonly trace: readonly unknown[] | null;\n}\n\n/** The figures of the engine's qoe stage, as `engine.qoe` gives them. */\nexport interface QoeFigures {\n /** Seconds from the load, or the first play if later, to the first frame playing. */\n readonly startupTime: number | null;\n readonly rebuffers: number;\n /** Seconds spent rebuffering, the current wait included. */\n readonly rebufferDuration: number;\n /** Video quality changes after the first selection. */\n readonly switches: number;\n}\n\n/** The optional namespaces the report reads, as the player reads them: a cast, once. */\ninterface Namespaces {\n readonly live?: { readonly latency: number | null };\n readonly qoe?: QoeFigures;\n readonly drm?: {\n readonly keySystem: string | null;\n readonly sessions: ReadonlyArray<{ readonly keyId: string; readonly status: string }>;\n };\n}\n\nfunction ranges(list: TimeRanges): RangeReport[] {\n return Array.from({ length: list.length }, (_, i) => ({\n start: list.start(i),\n end: list.end(i),\n }));\n}\n\nexport function playbackFacts(video: HTMLVideoElement): PlaybackReport {\n const quality = video.getVideoPlaybackQuality?.();\n return {\n currentTime: video.currentTime,\n duration: video.duration,\n paused: video.paused,\n ended: video.ended,\n seeking: video.seeking,\n readyState: video.readyState,\n networkState: video.networkState,\n playbackRate: video.playbackRate,\n volume: video.volume,\n muted: video.muted,\n videoWidth: video.videoWidth,\n videoHeight: video.videoHeight,\n buffered: ranges(video.buffered),\n seekable: ranges(video.seekable),\n frames:\n quality === undefined\n ? null\n : { decoded: quality.totalVideoFrames, dropped: quality.droppedVideoFrames },\n mediaError:\n video.error === null ? null : { code: video.error.code, message: video.error.message },\n };\n}\n\nexport function engineFacts(engine: Mattebox): EngineReport {\n const state = engine.stats.snapshot();\n const optional = engine as Namespaces;\n const error = engine.error;\n return {\n phase: state.lifecycle.phase,\n capabilities: [...engine.capabilities()],\n throughput: { slow: state.stats.throughputEwma, fast: state.stats.throughputFastEwma },\n bufferGoal: state.scheduling.bufferGoal,\n buffers: [...state.buffers.entries()].map(([id, buffer]) => ({\n id,\n codecs: buffer.codecs,\n ranges: buffer.ranges.map((range) => ({ start: range.start, end: range.end })),\n })),\n inflight: [...state.scheduling.inflight.values()].map((request) => ({\n trackId: request.trackId,\n seq: request.seq,\n url: request.url,\n })),\n live:\n state.live === null\n ? null\n : {\n start: state.live.span.start,\n end: state.live.span.end,\n edge: state.live.edge,\n latency: optional.live?.latency ?? null,\n },\n tracks: {\n active: Object.fromEntries(\n (['video', 'audio', 'text'] as const).map((type) => [\n type,\n engine.tracks.active(type)?.id ?? null,\n ]),\n ),\n available: engine.tracks.available.map((track) => ({\n id: track.id,\n contentType: track.contentType,\n mimeType: track.mimeType,\n lang: track.lang ?? null,\n role: track.role ?? null,\n renditions: track.renditions.length,\n })),\n },\n quality: {\n playing: engine.quality.playing?.id ?? null,\n active: engine.quality.active?.id ?? null,\n pinned: engine.quality.pinned,\n allowed: engine.quality.allowed.map((r) => r.id),\n renditions: engine.quality.renditions.map((r) => ({\n id: r.id,\n bitrate: r.bitrate,\n width: r.width ?? null,\n height: r.height ?? null,\n frameRate: r.frameRate ?? null,\n codecs: r.codecs,\n })),\n },\n drm:\n optional.drm === undefined\n ? null\n : { keySystem: optional.drm.keySystem, sessions: [...optional.drm.sessions] },\n qoe:\n optional.qoe === undefined\n ? null\n : {\n startupTime: optional.qoe.startupTime,\n rebuffers: optional.qoe.rebuffers,\n rebufferDuration: optional.qoe.rebufferDuration,\n switches: optional.qoe.switches,\n },\n error:\n error === null\n ? null\n : {\n category: error.category,\n code: error.code,\n fatal: error.fatal,\n recoverable: error.recoverable,\n context: error.context ?? null,\n },\n };\n}\n\n/** Two minutes of samples at two a second, and the marks of the same two minutes. */\nconst KEEP_SAMPLES = 240;\nconst KEEP_MS = 120_000;\n\nexport interface ReportSources {\n readonly player: PlayerHost;\n readonly samples: readonly Sample[];\n readonly marks: readonly Mark[];\n readonly counters: Counters;\n /** The trace entries kept, slimmed, oldest first. */\n readonly history: readonly unknown[];\n readonly support: Support | null;\n}\n\nexport function buildReport(sources: ReportSources): DiagnosticsReport {\n const { player } = sources;\n const engine = player.engine;\n const samples = sources.samples.slice(-KEEP_SAMPLES);\n const since = performance.now() - KEEP_MS;\n return {\n at: new Date().toISOString(),\n userAgent: navigator.userAgent,\n source: {\n url: player.getAttribute('src'),\n type: player.getAttribute('type'),\n handler: player.player?.session?.handler ?? null,\n },\n playback: playbackFacts(player.video),\n error: player.error,\n engine: engine === null ? null : engineFacts(engine),\n counters: sources.counters,\n samples,\n marks: sources.marks.filter((mark) => mark.t >= since),\n support: sources.support,\n trace: engine === null ? null : sources.history,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"report.js","names":[],"sources":["../src/report.ts"],"sourcesContent":["/**\n * The report: everything the panel shows, as one plain object a page can\n * send when something goes wrong, and the trace the engine's guide asks\n * for with every error report (chapter 12), as the element kept it: see\n * `trace.ts` for what is kept of each entry. Every field is serializable;\n * a native session has no engine part.\n */\nimport type { Mattebox } from 'mattebox';\nimport type { PlayerError, PlayerHost } from './host.js';\nimport type { Counters, Mark, Sample } from './sampler.js';\nimport type { Support } from './support.js';\n\nexport interface RangeReport {\n readonly start: number;\n readonly end: number;\n}\n\nexport interface PlaybackReport {\n readonly currentTime: number;\n readonly duration: number;\n readonly paused: boolean;\n readonly ended: boolean;\n readonly seeking: boolean;\n readonly readyState: number;\n readonly networkState: number;\n readonly playbackRate: number;\n readonly volume: number;\n readonly muted: boolean;\n readonly videoWidth: number;\n readonly videoHeight: number;\n readonly buffered: readonly RangeReport[];\n readonly seekable: readonly RangeReport[];\n readonly frames: { readonly decoded: number; readonly dropped: number } | null;\n readonly mediaError: { readonly code: number; readonly message: string } | null;\n}\n\nexport interface EngineReport {\n readonly phase: string;\n readonly capabilities: readonly string[];\n readonly throughput: { readonly slow: number; readonly fast: number };\n readonly bufferGoal: number;\n readonly buffers: ReadonlyArray<{\n readonly id: string;\n readonly codecs: string;\n readonly ranges: readonly RangeReport[];\n }>;\n readonly inflight: ReadonlyArray<{\n readonly trackId: string;\n readonly seq: number;\n readonly url: string;\n }>;\n readonly live: {\n readonly start: number;\n readonly end: number;\n readonly edge: number;\n readonly latency: number | null;\n } | null;\n readonly tracks: {\n readonly active: Readonly<Record<string, string | null>>;\n readonly available: ReadonlyArray<{\n readonly id: string;\n readonly contentType: string;\n readonly mimeType: string;\n readonly lang: string | null;\n readonly role: string | null;\n readonly renditions: number;\n }>;\n };\n readonly quality: {\n readonly playing: string | null;\n readonly active: string | null;\n readonly pinned: string | null;\n readonly allowed: readonly string[];\n readonly renditions: ReadonlyArray<{\n readonly id: string;\n readonly bitrate: number;\n readonly width: number | null;\n readonly height: number | null;\n readonly frameRate: number | null;\n readonly codecs: string | null;\n }>;\n };\n readonly drm: {\n readonly keySystem: string | null;\n readonly sessions: ReadonlyArray<{ readonly keyId: string; readonly status: string }>;\n } | null;\n /** The engine's qoe stage figures for this load, or null without the stage. */\n readonly qoe: QoeFigures | null;\n readonly error: {\n readonly category: string;\n readonly code: string;\n readonly fatal: boolean;\n readonly recoverable: boolean;\n readonly context: Readonly<Record<string, unknown>> | null;\n } | null;\n}\n\nexport interface DiagnosticsReport {\n /** When the report was taken, ISO 8601. */\n readonly at: string;\n readonly userAgent: string;\n readonly source: {\n readonly url: string | null;\n readonly type: string | null;\n /** The name of the handler that won, or null before a session. */\n readonly handler: string | null;\n };\n readonly playback: PlaybackReport;\n /** The player's fatal error of the current load, or null. */\n readonly error: PlayerError | null;\n /** Null for a native session. */\n readonly engine: EngineReport | null;\n readonly counters: Counters;\n /** The last two minutes of samples, and the marks in them. */\n readonly samples: readonly Sample[];\n readonly marks: readonly Mark[];\n /** What the browser supports, when it was probed before the report. */\n readonly support: Support | null;\n /** The trace entries the element kept, slimmed, oldest first, or null for a native session. */\n readonly trace: readonly unknown[] | null;\n /** The page's trackers on the player, with the ids support asks for. */\n readonly trackers: readonly TrackerReport[];\n}\n\nexport interface TrackerReport {\n readonly name: string;\n readonly session: string | null;\n readonly device: string | null;\n}\n\n/** The trackers of `player`, or none on a player older than trackers. */\nexport function trackersOf(player: PlayerHost): TrackerReport[] {\n const list = (player as Partial<PlayerHost>).trackers?.list ?? [];\n return list.map((tracker) => ({\n name: tracker.name,\n session: tracker.session ?? null,\n device: tracker.device ?? null,\n }));\n}\n\n/** The figures of the engine's qoe stage, as `engine.qoe` gives them. */\nexport interface QoeFigures {\n /** Seconds from the load, or the first play if later, to the first frame playing. */\n readonly startupTime: number | null;\n readonly rebuffers: number;\n /** Seconds spent rebuffering, the current wait included. */\n readonly rebufferDuration: number;\n /** Video quality changes after the first selection. */\n readonly switches: number;\n}\n\n/** The optional namespaces the report reads, as the player reads them: a cast, once. */\ninterface Namespaces {\n readonly live?: { readonly latency: number | null };\n readonly qoe?: QoeFigures;\n readonly drm?: {\n readonly keySystem: string | null;\n readonly sessions: ReadonlyArray<{ readonly keyId: string; readonly status: string }>;\n };\n}\n\nfunction ranges(list: TimeRanges): RangeReport[] {\n return Array.from({ length: list.length }, (_, i) => ({\n start: list.start(i),\n end: list.end(i),\n }));\n}\n\nexport function playbackFacts(video: HTMLVideoElement): PlaybackReport {\n const quality = video.getVideoPlaybackQuality?.();\n return {\n currentTime: video.currentTime,\n duration: video.duration,\n paused: video.paused,\n ended: video.ended,\n seeking: video.seeking,\n readyState: video.readyState,\n networkState: video.networkState,\n playbackRate: video.playbackRate,\n volume: video.volume,\n muted: video.muted,\n videoWidth: video.videoWidth,\n videoHeight: video.videoHeight,\n buffered: ranges(video.buffered),\n seekable: ranges(video.seekable),\n frames:\n quality === undefined\n ? null\n : { decoded: quality.totalVideoFrames, dropped: quality.droppedVideoFrames },\n mediaError:\n video.error === null ? null : { code: video.error.code, message: video.error.message },\n };\n}\n\nexport function engineFacts(engine: Mattebox): EngineReport {\n const state = engine.stats.snapshot();\n const optional = engine as Namespaces;\n const error = engine.error;\n return {\n phase: state.lifecycle.phase,\n capabilities: [...engine.capabilities()],\n throughput: { slow: state.stats.throughputEwma, fast: state.stats.throughputFastEwma },\n bufferGoal: state.scheduling.bufferGoal,\n buffers: [...state.buffers.entries()].map(([id, buffer]) => ({\n id,\n codecs: buffer.codecs,\n ranges: buffer.ranges.map((range) => ({ start: range.start, end: range.end })),\n })),\n inflight: [...state.scheduling.inflight.values()].map((request) => ({\n trackId: request.trackId,\n seq: request.seq,\n url: request.url,\n })),\n live:\n state.live === null\n ? null\n : {\n start: state.live.span.start,\n end: state.live.span.end,\n edge: state.live.edge,\n latency: optional.live?.latency ?? null,\n },\n tracks: {\n active: Object.fromEntries(\n (['video', 'audio', 'text'] as const).map((type) => [\n type,\n engine.tracks.active(type)?.id ?? null,\n ]),\n ),\n available: engine.tracks.available.map((track) => ({\n id: track.id,\n contentType: track.contentType,\n mimeType: track.mimeType,\n lang: track.lang ?? null,\n role: track.role ?? null,\n renditions: track.renditions.length,\n })),\n },\n quality: {\n playing: engine.quality.playing?.id ?? null,\n active: engine.quality.active?.id ?? null,\n pinned: engine.quality.pinned,\n allowed: engine.quality.allowed.map((r) => r.id),\n renditions: engine.quality.renditions.map((r) => ({\n id: r.id,\n bitrate: r.bitrate,\n width: r.width ?? null,\n height: r.height ?? null,\n frameRate: r.frameRate ?? null,\n codecs: r.codecs,\n })),\n },\n drm:\n optional.drm === undefined\n ? null\n : { keySystem: optional.drm.keySystem, sessions: [...optional.drm.sessions] },\n qoe:\n optional.qoe === undefined\n ? null\n : {\n startupTime: optional.qoe.startupTime,\n rebuffers: optional.qoe.rebuffers,\n rebufferDuration: optional.qoe.rebufferDuration,\n switches: optional.qoe.switches,\n },\n error:\n error === null\n ? null\n : {\n category: error.category,\n code: error.code,\n fatal: error.fatal,\n recoverable: error.recoverable,\n context: error.context ?? null,\n },\n };\n}\n\n/** Two minutes of samples at two a second, and the marks of the same two minutes. */\nconst KEEP_SAMPLES = 240;\nconst KEEP_MS = 120_000;\n\nexport interface ReportSources {\n readonly player: PlayerHost;\n readonly samples: readonly Sample[];\n readonly marks: readonly Mark[];\n readonly counters: Counters;\n /** The trace entries kept, slimmed, oldest first. */\n readonly history: readonly unknown[];\n readonly support: Support | null;\n}\n\nexport function buildReport(sources: ReportSources): DiagnosticsReport {\n const { player } = sources;\n const engine = player.engine;\n const samples = sources.samples.slice(-KEEP_SAMPLES);\n const since = performance.now() - KEEP_MS;\n return {\n at: new Date().toISOString(),\n userAgent: navigator.userAgent,\n source: {\n url: player.getAttribute('src'),\n type: player.getAttribute('type'),\n handler: player.player?.session?.handler ?? null,\n },\n playback: playbackFacts(player.video),\n error: player.error,\n engine: engine === null ? null : engineFacts(engine),\n counters: sources.counters,\n samples,\n marks: sources.marks.filter((mark) => mark.t >= since),\n support: sources.support,\n trace: engine === null ? null : sources.history,\n trackers: trackersOf(player),\n };\n}\n"],"mappings":"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"}
|
package/dist/sampler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { slimEntry } from "./trace.js";
|
|
2
|
-
|
|
2
|
+
// src/sampler.ts
|
|
3
3
|
/** Twenty minutes at two samples a second. */
|
|
4
4
|
const MAX_SAMPLES = 2400;
|
|
5
5
|
/** Trace entries kept, the engine's own default for a ring. */
|
|
@@ -190,7 +190,6 @@ function createSampler() {
|
|
|
190
190
|
}
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
|
-
//#endregion
|
|
194
193
|
export { createSampler };
|
|
195
194
|
|
|
196
195
|
//# sourceMappingURL=sampler.js.map
|
package/dist/style.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const STYLE = ":host{--mbx-chart-1:#5aa9e0;--mbx-chart-2:#6fbf8f;--mbx-chart-3:#d9a83f;--mbx-chart-4:#e05a6e;--mbx-chart-5:#a98fe0;display:inline-flex;position:relative}:host([hidden]){display:none}:host([inline]){display:block}[part~=button]{width:2.5em;height:2.5em;font:inherit;color:inherit;border-radius:var(--mbx-radius);cursor:pointer;opacity:.9;background:0 0;border:0;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}[part~=button]:hover{opacity:1}[part~=button]:focus{outline:2px solid var(--mbx-accent);outline-offset:-2px}[part~=button]:focus:not(:focus-visible){outline:none}[part~=icon],::slotted(*){fill:currentColor;width:1.75em;height:1.75em}:host([inline]) [part~=button]{display:none}[part~=panel]{box-sizing:border-box;width:46.6667em;color:var(--mbx-text,#f3ede4);border-radius:var(--mbx-radius,4px);font:400 .75em / 1.5 var(--mbx-mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);text-align:left;white-space:normal;background:#1c1c20f2;flex-direction:column;margin-bottom:.6667em;display:flex;position:absolute;bottom:100%;right:0;overflow:hidden;box-shadow:0 4px 16px #00000080}[part~=panel][hidden]{display:none}:host([inline]) [part~=panel]{width:auto;max-height:none;box-shadow:none;border-radius:0;margin:0;position:static}[part~=head]{border-bottom:1px solid #ffffff1f;align-items:center;padding:.5em;display:flex}[part~=tab],[part~=chart-tab]{font:inherit;color:var(--mbx-muted,#aaa49a);border-radius:var(--mbx-radius,4px);cursor:pointer;background:0 0;border:0;padding:.25em .6667em}[part~=tab][aria-selected=true],[part~=chart-tab][aria-selected=true]{color:var(--mbx-text,#f3ede4);background:#ffffff1f}[part~=copy],[part~=close],[part~=window]{font:inherit;color:inherit;border-radius:var(--mbx-radius,4px);cursor:pointer;background:0 0;border:1px solid #ffffff40;padding:.1667em .6667em}[part~=head]>*,[part~=tabs]>*{margin:0 .1667em}[part~=tabs]{align-items:center;margin:0;display:flex}[part~=head]>[part~=copy],[part~=chart-tabs]>[part~=window]{margin-left:auto}[part~=window] option{color:#000}[part~=tab]:focus,[part~=chart-tab]:focus,[part~=copy]:focus,[part~=close]:focus,[part~=window]:focus{outline:2px solid var(--mbx-accent,#e0543c);outline-offset:-2px}[part~=tab]:focus:not(:focus-visible){outline:none}[part~=chart-tab]:focus:not(:focus-visible){outline:none}[part~=copy]:focus:not(:focus-visible){outline:none}[part~=close]:focus:not(:focus-visible){outline:none}[part~=window]:focus:not(:focus-visible){outline:none}[part~=body]{overscroll-behavior:contain;min-height:0;padding:.6667em;overflow:auto}[part~=body]:focus{outline:2px solid var(--mbx-accent,#e0543c);outline-offset:-2px}[part~=body]:focus:not(:focus-visible){outline:none}[part~=page][hidden]{display:none}[part~=rows]{grid-template-columns:max-content 1fr;gap:.1667em 1em;margin:0;display:grid}[part~=key]{color:var(--mbx-muted,#aaa49a);white-space:nowrap}[part~=value]{overflow-wrap:anywhere;margin:0}[part~=heading]{letter-spacing:.06em;text-transform:uppercase;color:var(--mbx-muted,#aaa49a);margin:.9091em 0 .3636em;font-size:.9167em;font-weight:600}[part~=heading]:first-child{margin-top:0}[part~=note]{color:var(--mbx-muted,#aaa49a);margin:0}[part~=chart-tabs]{flex-wrap:wrap;align-items:center;margin:-.1667em -.1667em .3333em;display:flex}[part~=chart-tabs]>*,[part~=chart-tablist]>*{margin:.1667em}[part~=chart-tabs]>[part~=chart-tablist]{flex-wrap:wrap;margin:0;display:flex}[part~=chart]{width:100%;height:13.3333em;display:block}[part~=legend]{color:var(--mbx-muted,#aaa49a);flex-wrap:wrap;margin:.25em -.5em -1px;display:flex}[part~=legend]>*{margin:1px .5em}[part~=swatch]{vertical-align:middle;border-radius:2px;width:.8333em;height:.8333em;margin-right:.3333em;display:inline-block}[part~=readout]{margin-top:.3333em}[part~=table]{border-collapse:collapse;width:100%;margin-bottom:.3333em}[part~=cell]{text-align:left;white-space:nowrap;border-bottom:1px solid #ffffff14;padding:.1667em .5em;font-weight:400}[part~=label]{color:var(--mbx-muted,#aaa49a)}[part~=ok]{color:var(--mbx-chart-2)}[part~=bad]{color:var(--mbx-chart-4)}[part~=na]{color:var(--mbx-muted,#aaa49a)}[part~=ok],[part~=bad],[part~=na],[part~=maybe]{font-weight:600}";
|
|
2
|
-
//#endregion
|
|
3
2
|
export { STYLE };
|
|
4
3
|
|
|
5
4
|
//# sourceMappingURL=style.js.map
|
package/dist/support.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/support.ts
|
|
2
2
|
const CODECS = [
|
|
3
3
|
{
|
|
4
4
|
label: "H.264 High 4.0",
|
|
@@ -330,7 +330,6 @@ function probeSupport() {
|
|
|
330
330
|
}))();
|
|
331
331
|
return cached;
|
|
332
332
|
}
|
|
333
|
-
//#endregion
|
|
334
333
|
export { probeSupport };
|
|
335
334
|
|
|
336
335
|
//# sourceMappingURL=support.js.map
|
package/dist/trace.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/trace.ts
|
|
2
2
|
/** Segments as a count, whatever their addressing. */
|
|
3
3
|
function segmentCount(segments) {
|
|
4
4
|
if (Array.isArray(segments)) return segments.length;
|
|
@@ -93,7 +93,6 @@ function slimEntry(entry) {
|
|
|
93
93
|
};
|
|
94
94
|
return JSON.parse(JSON.stringify(slim, serializable));
|
|
95
95
|
}
|
|
96
|
-
//#endregion
|
|
97
96
|
export { slimEntry };
|
|
98
97
|
|
|
99
98
|
//# sourceMappingURL=trace.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mattebox/player-diagnostics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "<mbx-diagnostics>: playback stats, charts, engine state and browser support inside <mattebox-player>, and a report to send.",
|
|
5
5
|
"author": "Josep Boix Requesens",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"modern": "./dist/index.js",
|
|
35
35
|
"default": "./dist/es2015/index.js"
|
|
36
36
|
},
|
|
37
|
+
"./locales/*": {
|
|
38
|
+
"types": "./dist/locales/*.d.ts",
|
|
39
|
+
"modern": "./dist/locales/*.js",
|
|
40
|
+
"default": "./dist/es2015/locales/*.js"
|
|
41
|
+
},
|
|
37
42
|
"./package.json": "./package.json"
|
|
38
43
|
},
|
|
39
44
|
"files": [
|
|
@@ -49,10 +54,10 @@
|
|
|
49
54
|
"release:ci": "semantic-release"
|
|
50
55
|
},
|
|
51
56
|
"devDependencies": {
|
|
52
|
-
"@mattebox/player-core": "^0.
|
|
57
|
+
"@mattebox/player-core": "^0.8.0"
|
|
53
58
|
},
|
|
54
59
|
"peerDependencies": {
|
|
55
|
-
"@mattebox/player": ">=0.
|
|
56
|
-
"mattebox": "^0.16.
|
|
60
|
+
"@mattebox/player": ">=0.12.0",
|
|
61
|
+
"mattebox": "^0.16.2"
|
|
57
62
|
}
|
|
58
63
|
}
|