diff --git a/docs/superpowers/specs/2026-07-30-structured-hls-diagnostics-design.md b/docs/superpowers/specs/2026-07-30-structured-hls-diagnostics-design.md new file mode 100644 index 000000000..bea4b0c9d --- /dev/null +++ b/docs/superpowers/specs/2026-07-30-structured-hls-diagnostics-design.md @@ -0,0 +1,210 @@ +# Structured HLS Diagnostics + +## Context + +PR #1314 made native media diagnostics evidence-based, but the HLS path still +normalizes `type`, `details`, `message`, and an arbitrary `error` payload into +one string. `classifyHlsPlaybackIssue` then searches that string for broad +fragments such as `network`, `status`, and `codec`. This can misclassify a +provider message, and serializing arbitrary values can retain URLs, +credentials, response content, loader context, or other provider data. + +IPTVnator currently installs hls.js 1.6.16. Its public `ErrorData` supplies +structured `type`, `details`, `fatal`, and an optional `response.code`. +Specific `ErrorDetails` values also identify the manifest, level, segment, key, +or media stage. The same payload contains unsafe or unbounded fields, including +`error`, `reason`, `networkDetails`, `context`, `loader`, `url`, and response +URL/text/data, which are not needed for classification. + +Both the HTML5 player and ArtPlayer subscribe to `Hls.Events.ERROR`. They +currently ignore `fatal: false` before calling the classifier, but each adapter +builds the broad string input independently. + +## Goals + +- Create one structured HLS evidence boundary for the installed hls.js error + shape. +- Retain only allowlisted engine type/detail identifiers, the final + fatal/recoverable disposition, a stage derived from exact details, a + structured failure kind, and a validated HTTP status when present. +- Classify HLS failures from exact engine values rather than substring + heuristics. +- Keep insufficient evidence explicitly unknown. +- Ensure recoverable hls.js events never become terminal playback diagnostics. +- Use the same extraction and classification path in HTML5 and ArtPlayer. +- Show only the sanitized evidence in technical diagnostic details. + +## Non-goals + +- Diagnostic history, persistence, correlation, or telemetry. +- Additional manifest or stream probes. +- Automatic player failover. +- A Shaka or mpegts.js diagnostic redesign. +- A cross-player confidence or likelihood matrix. +- Inferring browser access failures from status zero, `TypeError`, or error + message text. +- Changing hls.js retry, level-switch, or recovery behavior. + +## Approaches Considered + +### Boundary sanitizer (selected) + +Convert hls.js `ErrorData` into a small `HlsPlaybackEvidence` value at the +adapter boundary. The classifier accepts only that value. This centralizes the +allowlist, makes unsafe fields structurally unavailable to classification and +UI code, and gives HTML5 and ArtPlayer identical behavior. + +### Pass `ErrorData` directly + +This would preserve the strongest TypeScript relationship with hls.js, but it +would leave every consumer able to read or render URLs, loader state, headers, +response payloads, and arbitrary errors. It also makes privacy depend on each +future call site remembering the same exclusions. + +### Keep the broad input and tighten string tests + +Exact comparisons could improve classification, but the adapters would still +copy arbitrary messages and objects into diagnostics. This would not establish +the requested safe evidence contract. + +## Evidence Contract + +`HlsPlaybackEvidence` contains: + +- `engineType`: one of the installed hls.js `ErrorTypes` values, otherwise + `unknown`; +- `engineDetails`: one of the installed hls.js `ErrorDetails` values, + including its existing `unknown` value; +- `disposition`: `fatal` when `ErrorData.fatal === true`, otherwise + `recoverable`; +- `stage`: `manifest`, `level`, `segment`, `key`, `media`, or `unknown`; +- `failure`: `http`, `timeout`, `network`, `access`, or `unknown`; +- optional `httpStatus`: an integer from 100 through 599 copied only from + `ErrorData.response.code`. + +The `access` value is part of the contract but hls.js 1.6.16 does not expose a +reliable structured CORS, mixed-content, CSP, or private-network-access code. +Status zero and generic fetch failures can also mean DNS, offline, connection, +or abort failures. The extractor therefore does not emit `access` for current +runtime shapes; it uses `network` or `unknown` and never inspects message text. + +The extractor does not copy: + +- `ErrorData.url`; +- `context`, including request URLs and headers; +- `networkDetails` or loader instances; +- `response.url`, `response.text`, or `response.data`; +- `error`, deprecated `err`, `reason`, events, fragments, or arbitrary values. + +The existing playback metadata still carries the active source URL for the +pre-existing Retry, Copy URL, and explicit external-player workflows. No HLS +error URL is copied into evidence or shown in technical details. + +## Stage Mapping + +Stages are derived only from exact `ErrorDetails` values: + +- `manifest`: manifest load, timeout, parsing, and incompatible-codec details; +- `level`: level load, timeout, parsing, empty, and switch details, plus + audio/subtitle track playlist load details; +- `segment`: fragment load, timeout, parsing, decrypt, and gap details; +- `key`: key load/timeout and key-system details; +- `media`: buffer, mux/remux, attach-media, and playback-stall details; +- `unknown`: internal, aborted, interstitial, unrecognized, or otherwise + unmapped details. + +This mapping describes the engine stage, not root-cause likelihood. For +example, a segment-stage timeout is still classified as a network timeout. + +## Failure Mapping + +Failure kind uses this precedence: + +1. Exact timeout details produce `timeout`. +2. A validated 4xx or 5xx `response.code` produces `http`. +3. Exact network load-error details, or the exact `networkError` engine type, + produce `network`. +4. No current hls.js 1.6.16 field independently proves `access`. +5. Everything else is `unknown`. + +A 1xx, 2xx, or 3xx status remains retained as factual response metadata but +does not by itself produce an HTTP-failure kind. + +## Classification + +`classifyHlsPlaybackIssue` returns `PlaybackDiagnostic | null`: + +1. `recoverable` returns `null` before classification. +2. Exact incompatible/add-codec details produce `unsupported-codec`. +3. Exact key-system or fragment-decrypt evidence produces + `drm-or-encryption`. +4. Exact network type, load/timeout details, or HTTP failure evidence produces + `network-error`. +5. Exact media or mux type produces `media-decode-error`. +6. Everything else produces `unknown-playback-error`. + +The HLS path does not emit `browser-access-error` without structured access +evidence. It also does not classify `keyLoadError` as DRM: failure to fetch an +encryption key is network evidence, while `fragDecryptError` and key-system +details are encryption/DRM evidence. + +The normalized `PlaybackDiagnostic` retains the sanitized HLS evidence and +copies its validated HTTP status into the existing top-level `httpStatus` +field. External fallback recommendations continue to derive from the final +diagnostic code. + +## Adapter Flow + +HTML5 and ArtPlayer both use: + +1. `createHlsPlaybackEvidence(data)`; +2. `classifyHlsPlaybackIssue(evidence, sourceMetadata)`; +3. emit only when the classifier returns a diagnostic. + +The fatal gate therefore lives in the shared classifier rather than being +duplicated in the two adapters. hls.js registers its final error-controller +listener during construction, before IPTVnator subscribes, so the application +observes the final `fatal` value after retry/alternate handling has had a +chance to update it. + +## User Interface + +The existing technical “Error details” row renders a deterministic, safe HLS +summary made only from the evidence fields, for example: + +`stage=manifest · failure=http · type=networkError · details=manifestLoadError · disposition=fatal · HTTP 404` + +No HLS error message, URL, header, body, response text, or arbitrary provider +payload appears. Existing title/description and HTTP metadata rendering remain +unchanged, so no new translation keys or layout changes are required. + +## Testing + +Use test-driven development: + +- Add extractor tests using hls.js 1.6.16-shaped manifest, level, segment, key, + media, HTTP, timeout, and unknown payloads. +- Prove unrecognized runtime values become `unknown`. +- Prove response URLs/text/data, request context/headers, error messages, + network details, and credential-shaped payloads do not appear in evidence or + rendered details. +- Add classifier tests for exact codec, DRM, network, media, and unknown + values. +- Prove misleading strings cannot override structured engine values. +- Prove recoverable events return `null`. +- Update HTML5 and ArtPlayer integration tests to cover fatal HTTP evidence and + recoverable suppression through the shared path. +- Add a focused view test for the deterministic safe HLS summary. + +Run the complete `ui-playback` unit target, its lint target, the web typecheck, +i18n drift validation, and release-note validation. No E2E is required because +the player workflow and diagnostic controls are unchanged; adapter, classifier, +and rendered-detail behavior are covered at their closest unit/component +boundaries. + +## Documentation And Release Note + +Update `docs/architecture/embedded-inline-playback.md`, the canonical playback +diagnostic description, to replace raw HLS details with the structured +contract. Add a `fix(playback)` release note because users receive more accurate +HLS diagnoses and safer technical details.