From d0694a33c4cb9651fb49a37f25ed907c9c662ccd Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 31 Jul 2026 09:36:44 +0200 Subject: [PATCH] docs(playback): design structured Shaka diagnostics --- ...2026-07-31-structured-shaka-diagnostics.md | 714 ++++++++++++++++++ ...-31-structured-shaka-diagnostics-design.md | 286 +++++++ 2 files changed, 1000 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-31-structured-shaka-diagnostics.md create mode 100644 docs/superpowers/specs/2026-07-31-structured-shaka-diagnostics-design.md diff --git a/docs/superpowers/plans/2026-07-31-structured-shaka-diagnostics.md b/docs/superpowers/plans/2026-07-31-structured-shaka-diagnostics.md new file mode 100644 index 000000000..79024d625 --- /dev/null +++ b/docs/superpowers/plans/2026-07-31-structured-shaka-diagnostics.md @@ -0,0 +1,714 @@ +# Structured Shaka Diagnostics Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace raw Shaka error retention with a version-locked, public, +allowlisted evidence boundary that distinguishes recoverable events from +terminal load failures. + +**Architecture:** Normalize only Shaka 5.2.2 public severity/category/code +values and documented HTTP status layouts into `ShakaPlaybackEvidence`. +`ShakaVideoSession` supplies the lifecycle disposition, the classifier maps +only exact category/code pairs, and the existing UI renders only structured +evidence. + +**Tech Stack:** Angular 21, TypeScript 5.9, Shaka Player 5.2.2, Jest through +Nx, Markdown architecture and release-note documentation. + +--- + +### Task 0: Establish The Evidence And Baseline + +**Files:** + +- Verify: `package.json` +- Verify: `pnpm-lock.yaml` +- Verify: `node_modules/shaka-player/lib/util/error.js` +- Verify: `node_modules/shaka-player/lib/net/http_plugin_utils.js` +- Verify: `node_modules/shaka-player/lib/net/networking_engine.js` +- Verify: `node_modules/shaka-player/lib/media/preload_manager.js` +- Verify: `node_modules/shaka-player/lib/player.js` +- Verify: `node_modules/shaka-player/dist/shaka-player.compiled.d.ts` + +- [x] **Step 1: Install locked dependencies** + +Run: + +```bash +pnpm install --frozen-lockfile +``` + +Expected: exit 0, Shaka Player `5.2.2`, and no lockfile change. + +- [x] **Step 2: Verify Nx workspace discovery** + +Run: + +```bash +pnpm nx show projects +``` + +Expected: exit 0 and output containing `ui-playback`, `web`, and `web-e2e`. + +- [x] **Step 3: Run the affected-project baseline** + +Run: + +```bash +pnpm nx test ui-playback +``` + +Expected: 86 suites and 801 tests pass before implementation. + +- [x] **Step 4: Audit the installed public runtime** + +Confirm: + +```text +shaka.Player.version = v5.2.2 +Severity = { RECOVERABLE: 1, CRITICAL: 2 } +Category = { + NETWORK: 1, TEXT: 2, MEDIA: 3, MANIFEST: 4, STREAMING: 5, + DRM: 6, PLAYER: 7, CAST: 8, STORAGE: 9, ADS: 10 +} +BAD_HTTP_STATUS data[1] = HTTP status +LICENSE_REQUEST_FAILED data[0] = nested Shaka network error +SERVER_CERTIFICATE_REQUEST_FAILED data[0] = nested Shaka network error +``` + +Confirm the installed event order: + +```text +recoverable Player#error -> engine continues +critical PreloadManager error -> public error event -> load rejection +direct manifest/parser throw -> load rejection without required error event +attempts exhausted -> final recoverable-severity error rejects load +``` + +### Task 1: Drive The Public Shaka Evidence Boundary From Failing Tests + +**Files:** + +- Create: + `libs/ui/playback/src/lib/shaka-engine/shaka-error-contract.ts` +- Create: + `libs/ui/playback/src/lib/shaka-engine/shaka-playback-evidence.util.ts` +- Create: + `libs/ui/playback/src/lib/shaka-engine/shaka-playback-evidence.util.spec.ts` +- Modify: + `libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.model.ts` +- Modify: + `libs/ui/playback/src/lib/shaka-engine/shaka-module.types.ts` + +- [ ] **Step 1: Write the failing installed-runtime contract test** + +In `shaka-playback-evidence.util.spec.ts`, load the real compiled package in a +child process with `global.self = globalThis` and return: + +```typescript +interface InstalledShakaContract { + readonly version: string; + readonly severity: Readonly>; + readonly category: Readonly>; + readonly code: Readonly>; +} +``` + +Assert: + +```typescript +expect(installed.version).toBe(SHAKA_DIAGNOSTIC_VERSION); +expect(installed.severity).toEqual(SHAKA_ERROR_SEVERITY); +expect(installed.category).toEqual(SHAKA_ERROR_CATEGORY); +for (const [name, value] of Object.entries(SHAKA_ERROR_CODE)) { + expect(installed.code[name]).toBe(value); +} +``` + +Run: + +```bash +pnpm exec jest \ + libs/ui/playback/src/lib/shaka-engine/shaka-playback-evidence.util.spec.ts \ + --config libs/ui/playback/jest.config.ts --runInBand +``` + +Expected: FAIL because the contract and evidence module do not exist. + +- [ ] **Step 2: Add the minimal public model** + +Add const-derived types to `playback-diagnostics.model.ts`: + +```typescript +export const ShakaPlaybackSeverity = { + Recoverable: 'recoverable', + Critical: 'critical', + Unknown: 'unknown', +} as const; + +export const ShakaPlaybackCategory = { + Network: 'network', + Text: 'text', + Media: 'media', + Manifest: 'manifest', + Streaming: 'streaming', + Drm: 'drm', + Player: 'player', + Cast: 'cast', + Storage: 'storage', + Ads: 'ads', + Unknown: 'unknown', +} as const; + +export const ShakaPlaybackDisposition = { + Terminal: 'terminal', + Recoverable: 'recoverable', +} as const; + +export const ShakaPlaybackStage = { + Manifest: 'manifest', + Segment: 'segment', + Media: 'media', + License: 'license', + Unknown: 'unknown', +} as const; + +export const ShakaPlaybackFailure = { + Network: 'network', + Drm: 'drm', + Manifest: 'manifest', + Media: 'media', + Unknown: 'unknown', +} as const; + +export const ShakaPlaybackUnknownCode = 'unknown' as const; + +export interface ShakaPlaybackEvidence { + readonly severity: ShakaPlaybackSeverity; + readonly category: ShakaPlaybackCategory; + readonly engineCode: number | typeof ShakaPlaybackUnknownCode; + readonly disposition: ShakaPlaybackDisposition; + readonly stage: ShakaPlaybackStage; + readonly failure: ShakaPlaybackFailure; + readonly httpStatus?: number; +} +``` + +Add `readonly shaka?: ShakaPlaybackEvidence` to `PlaybackDiagnostic`, and +remove `message` from `ShakaErrorLike` so downstream Shaka code has no typed +message access: + +```typescript +export interface ShakaErrorLike { + severity: number; + category: number; + code: number; + data?: readonly unknown[]; +} +``` + +- [ ] **Step 3: Add the exact 5.2.2 public allowlist** + +In `shaka-error-contract.ts`, export: + +```typescript +export const SHAKA_DIAGNOSTIC_VERSION = 'v5.2.2'; +export const SHAKA_ERROR_SEVERITY = { + RECOVERABLE: 1, + CRITICAL: 2, +} as const; +export const SHAKA_ERROR_CATEGORY = { + NETWORK: 1, + TEXT: 2, + MEDIA: 3, + MANIFEST: 4, + STREAMING: 5, + DRM: 6, + PLAYER: 7, + CAST: 8, + STORAGE: 9, + ADS: 10, +} as const; +``` + +Define `SHAKA_ERROR_CODE` with the exact active public Shaka 5.2.2 online +playback codes from categories NETWORK through PLAYER, including the direct +and nested HTTP codes, media and manifest codes, all DRM codes, and +`LOAD_INTERRUPTED`. Do not include retired numeric gaps. The contract test +must verify every name/value against the installed runtime. + +- [ ] **Step 4: Write failing sanitizer and privacy tests** + +Use the documented direct bad-status shape: + +```typescript +const raw = { + severity: 1, + category: 1, + code: 1001, + message: 'https://user:secret@provider.example/manifest.mpd', + data: [ + 'https://provider.example/manifest.mpd?token=secret', + 503, + 'provider body secret', + { Authorization: 'Bearer secret' }, + 0, + 'https://provider.example/final?token=secret', + ], +}; +``` + +Expect: + +```typescript +{ + severity: 'recoverable', + category: 'network', + engineCode: 1001, + disposition: 'terminal', + stage: 'unknown', + failure: 'network', + httpStatus: 503, +} +``` + +Add a nested `DRM + LICENSE_REQUEST_FAILED` shape whose `data[0]` is the same +network error and whose remaining data contains license/session secrets. +Expect only the nested status number to survive. + +Cover: + +- HTTP boundaries 99/100/599/600 and non-integers; +- status-like values on non-documented codes remain absent; +- unknown severity/category/code values become `unknown`; +- mismatched public category/code pairs have `failure=unknown`; +- no URL, message, header, body, key, license, credential, or arbitrary + property survives `JSON.stringify(evidence)`; +- exact manifest, segment, media, license, and unknown stages; +- exact network, DRM, manifest, media, restrictions, and unknown failures. + +- [ ] **Step 5: Implement the minimal sanitizer** + +In `shaka-playback-evidence.util.ts`, expose: + +```typescript +export function createShakaPlaybackEvidence( + error: Partial | null | undefined, + disposition: ShakaPlaybackDisposition +): ShakaPlaybackEvidence; +``` + +Use only: + +```typescript +error?.severity; +error?.category; +error?.code; +error?.data?.[1]; // exact direct BAD_HTTP_STATUS only +error?.data?.[0]; // exact documented nested DRM network error only +``` + +Build severity/category/code through exact sets, derive stage/failure through +exact category/code pairs, and return a fresh flat evidence object. Do not +read or serialize any other field. + +Run the focused spec again. + +Expected: PASS. + +### Task 2: Drive Exact Classification And Shared Diagnostic Retention + +**Files:** + +- Modify: + `libs/ui/playback/src/lib/shaka-engine/shaka-error-classifier.spec.ts` +- Modify: + `libs/ui/playback/src/lib/shaka-engine/shaka-error-classifier.ts` +- Modify: + `libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.ts` + +- [ ] **Step 1: Replace heuristic expectations with failing exact-code tests** + +Change the classifier API to: + +```typescript +classifyShakaPlaybackIssue( + error, + metadata, + disposition +): PlaybackDiagnostic | null; +``` + +Add expectations: + +```text +NETWORK + BAD_HTTP_STATUS -> network-error +DRM + REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE -> drm-or-encryption +MANIFEST + DASH_NO_COMMON_KEY_SYSTEM -> drm-or-encryption +MANIFEST + DASH_INVALID_XML -> unknown-playback-error +MANIFEST + DASH_UNSUPPORTED_CONTAINER -> unsupported-container +MANIFEST + CONTENT_UNSUPPORTED_BY_BROWSER -> unknown-playback-error +MEDIA + MEDIA_SOURCE_OPERATION_FAILED -> media-decode-error +MANIFEST + RESTRICTIONS_CANNOT_BE_MET -> unknown-playback-error +mismatched category/code -> unknown-playback-error +unknown values -> unknown-playback-error +recoverable disposition -> null +``` + +Put misleading `message` and `data` strings such as `CORS codec license` +alongside unknown or mismatched structured values and prove they do not change +classification. + +Run the focused classifier spec. + +Expected: FAIL against the current category/message heuristic classifier. + +- [ ] **Step 2: Implement exact classification** + +Create evidence first: + +```typescript +const evidence = createShakaPlaybackEvidence(error, disposition); +if (evidence.disposition === ShakaPlaybackDisposition.Recoverable) { + return null; +} +``` + +Select the top-level code only from exact evidence: + +```typescript +network -> NetworkError +drm -> DrmOrEncryption +DASH_UNSUPPORTED_CONTAINER -> UnsupportedContainer +MEDIA_SOURCE_OPERATION_FAILED -> MediaDecodeError +MEDIA_SOURCE_OPERATION_THREW -> MediaDecodeError +VIDEO_ERROR -> MediaDecodeError +everything else -> UnknownPlaybackError +``` + +Call `createPlaybackDiagnostic` with `httpStatus` and `shaka`, but no raw +`details` or native message. + +Extend `createPlaybackDiagnostic` to accept and retain: + +```typescript +readonly shaka?: ShakaPlaybackEvidence; +``` + +Run the focused evidence and classifier specs. + +Expected: PASS. + +- [ ] **Step 3: Sanitize the pre-engine DRM diagnostic** + +Change `createUnsupportedDrmDiagnostic` to ignore the provider-supplied +license string in technical details: + +```typescript +details: 'Unsupported DRM license configuration'; +``` + +Add a secret-bearing license string test and prove the diagnostic JSON and +details omit it while the top-level code remains `drm-or-encryption` and +external fallback remains disabled. + +### Task 3: Drive Session Lifecycle And Adapter Routing + +**Files:** + +- Modify: + `libs/ui/playback/src/lib/shaka-engine/shaka-video-session.spec.ts` +- Modify: + `libs/ui/playback/src/lib/shaka-engine/shaka-video-session.ts` +- Modify: + `libs/ui/playback/src/lib/shaka-engine/shaka-player-test-double.ts` +- Modify: + `libs/ui/playback/src/lib/art-player/art-player-source-session.dash.spec.ts` + +- [ ] **Step 1: Write failing recoverable/terminal routing tests** + +Add session regressions for: + +```text +recoverable Player#error -> no diagnostic, same current player +unknown-severity Player#error -> no diagnostic, same current player +critical Player#error -> one terminal diagnostic, player destroyed +recoverable-severity load rejection -> one terminal diagnostic +critical event during stalled load -> one diagnostic, later interruption ignored +module-loader Error with URL/token -> terminal unknown evidence without message +``` + +For the terminal rejected `BAD_HTTP_STATUS`, expect: + +```typescript +expect(issue.shaka).toEqual({ + severity: 'recoverable', + category: 'network', + engineCode: 1001, + disposition: 'terminal', + stage: 'unknown', + failure: 'network', + httpStatus: 503, +}); +``` + +Run: + +```bash +pnpm exec jest \ + libs/ui/playback/src/lib/shaka-engine/shaka-video-session.spec.ts \ + --config libs/ui/playback/jest.config.ts --runInBand +``` + +Expected: FAIL because the current session suppresses only event severity, +passes arbitrary messages on rejection, and has no structured disposition. + +- [ ] **Step 2: Implement route-owned disposition** + +In `ShakaVideoSession`: + +- call the classifier with `Terminal` for module rejection, unsupported + browser support, and `load()` rejection; +- call it with `Recoverable` for exact recoverable events and emit nothing + when it returns null; +- call it with `Terminal` only for exact critical events; +- ignore unknown-severity events; +- suppress interruption only for the exact + `CRITICAL + PLAYER + LOAD_INTERRUPTED` triple; +- remove `toErrorMessage` and every Shaka message fallback; +- tear down only after a non-null terminal diagnostic. + +Run the focused session spec. + +Expected: PASS. + +- [ ] **Step 3: Write and pass the ArtPlayer adapter regression** + +Configure the fake Shaka player's load promise to reject with a documented +bad-status shape, pass `emitPlaybackIssue` into +`ArtPlayerSourceSession`, invoke the `mpd` custom type, and expect one +diagnostic with: + +```typescript +{ + source: PlaybackDiagnosticSource.Shaka, + player: InlinePlaybackPlayer.ArtPlayer, + code: PlaybackDiagnosticCode.NetworkError, + httpStatus: 503, + shaka: expect.objectContaining({ + engineCode: 1001, + disposition: 'terminal', + }), +} +``` + +Prove the response body/header/token sentinel is absent from serialized +diagnostic data other than the pre-existing active `sourceUrl`. + +### Task 4: Drive Safe Rendered Technical Details + +**Files:** + +- Modify: + `libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts` +- Modify: + `libs/ui/playback/src/lib/web-player-view/web-player-view-diagnostics.utils.ts` + +- [ ] **Step 1: Write the failing rendered-detail regression** + +Create a Shaka diagnostic with safe evidence plus malicious legacy fields: + +```typescript +details: 'Authorization: Bearer shaka-render-secret', +nativeErrorMessage: + 'https://provider.example/license?token=shaka-render-secret', +shaka: { + severity: 'recoverable', + category: 'network', + engineCode: 1001, + disposition: 'terminal', + stage: 'manifest', + failure: 'network', + httpStatus: 503, +}, +``` + +Expect the error-details row to equal: + +```text +stage=manifest · failure=network · severity=recoverable · category=network · code=1001 · disposition=terminal · HTTP 503 +``` + +Prove the rendered values omit the secret, provider hostname, +`Authorization`, raw body text, and legacy details. + +Run the focused component spec. + +Expected: FAIL because the UI has no Shaka structured branch. + +- [ ] **Step 2: Render only Shaka evidence** + +Add the Shaka branch before the HLS/VHS/legacy branches: + +```typescript +if (issue.shaka) { + return [ + `stage=${issue.shaka.stage}`, + `failure=${issue.shaka.failure}`, + `severity=${issue.shaka.severity}`, + `category=${issue.shaka.category}`, + `code=${issue.shaka.engineCode}`, + `disposition=${issue.shaka.disposition}`, + issue.shaka.httpStatus === undefined + ? '' + : `HTTP ${issue.shaka.httpStatus}`, + ] + .filter((value) => value.length > 0) + .join(' · '); +} +``` + +Run the focused component spec. + +Expected: PASS. + +### Task 5: Documentation, Release Note, And Full Validation + +**Files:** + +- Modify: `docs/architecture/embedded-inline-playback.md` +- Modify: `docs/architecture/m3u-playlist-module.md` +- Modify: `CLAUDE.md` +- Modify: `AGENTS.md` +- Create: `.changes/playback-structured-shaka-diagnostics.md` + +- [ ] **Step 1: Update canonical documentation** + +Document: + +- Shaka 5.2.2 public version lock; +- evidence fields and rejected raw fields; +- direct and nested documented HTTP status layouts; +- exact failure/stage classification and ambiguous unknown cases; +- recoverable event versus terminal load-rejection semantics; +- sanitized technical detail output; +- unchanged source routing, ClearKey fallback rule, and VOD failover. + +Keep the high-level Shaka summaries in `CLAUDE.md` and `AGENTS.md` +consistent. + +- [ ] **Step 2: Add the user-facing release note** + +Create: + +```markdown +--- +type: fix +area: playback +--- + +Shaka playback errors now use exact engine evidence, keep recoverable errors from interrupting playback, and omit provider URLs, credentials, and response data from technical details. +``` + +- [ ] **Step 3: Run focused and affected validation** + +Run: + +```bash +pnpm exec jest \ + libs/ui/playback/src/lib/shaka-engine/shaka-playback-evidence.util.spec.ts \ + libs/ui/playback/src/lib/shaka-engine/shaka-error-classifier.spec.ts \ + libs/ui/playback/src/lib/shaka-engine/shaka-video-session.spec.ts \ + libs/ui/playback/src/lib/art-player/art-player-source-session.dash.spec.ts \ + libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts \ + --config libs/ui/playback/jest.config.ts --runInBand +pnpm nx test ui-playback +pnpm nx lint ui-playback +pnpm nx typecheck web +pnpm run i18n:check +pnpm run release:notes:validate +git diff --check +``` + +Expected: every command exits 0. + +- [ ] **Step 4: Complete the test-impact pass** + +Confirm: + +```text +Affected project: ui-playback +Unit/component coverage: required and run +Lint: required and run +Web typecheck: required and run +i18n validation: required and run +Release-note validation: required and run +E2E: not required; no workflow, routing, player selection, or integration + lifecycle changed, and event/rejection behavior is covered at the + session plus ArtPlayer adapter boundaries +``` + +### Task 6: Independent Review, Fixes, And Ready PR + +**Files:** + +- Review: complete diff from `origin/master` to branch HEAD + +- [ ] **Step 1: Commit the implementation before review** + +Create focused conventional commits for the evidence boundary/runtime change +and documentation/release note. Confirm: + +```bash +git status --short +git log --oneline origin/master..HEAD +``` + +- [ ] **Step 2: Dispatch an independent local Codex review** + +Give the reviewer: + +```text +Base: origin/master +Head: branch HEAD +Requirements: the structured Shaka design and this implementation plan +Focus: actionable P0/P1/P2 correctness, privacy, public-contract drift, + lifecycle duplication/suppression, classification accuracy, and tests +``` + +Require a full-diff review. If the reviewer reports an issue, verify it +against the installed Shaka 5.2.2 source before changing code. + +- [ ] **Step 3: Fix confirmed findings through TDD** + +For every confirmed P0/P1/P2: + +1. add or tighten a regression test; +2. run it and observe the expected failure; +3. implement the minimal fix; +4. rerun the focused test and affected suite; +5. commit the fix. + +- [ ] **Step 4: Repeat independent review** + +Review the updated full diff from `origin/master`. Expected: no actionable +P0/P1/P2 findings. + +- [ ] **Step 5: Repeat the complete validation ladder** + +Rerun every command from Task 5 Step 3 after the final review fix. Expected: +all exit 0 with fresh output. + +- [ ] **Step 6: Push and create a ready PR** + +Push `agent/structured-shaka-diagnostics` and create a non-draft PR with: + +```text +Title: fix(playback): structure Shaka diagnostics +Base: master +Summary: exact Shaka 5.2.2 evidence, lifecycle-aware recoverable handling, + safe technical details +Testing: list every fresh validation command and result +``` + +Inspect the created PR and confirm it is ready for review. diff --git a/docs/superpowers/specs/2026-07-31-structured-shaka-diagnostics-design.md b/docs/superpowers/specs/2026-07-31-structured-shaka-diagnostics-design.md new file mode 100644 index 000000000..0778df038 --- /dev/null +++ b/docs/superpowers/specs/2026-07-31-structured-shaka-diagnostics-design.md @@ -0,0 +1,286 @@ +# Structured Shaka Diagnostics + +## Context + +PR #1314 stopped treating an ambiguous native `MediaError` as codec evidence +and retained explicit HTTP evidence. PR #1316 added an allowlisted hls.js +boundary for HTML5 and ArtPlayer. PR #1317 added an allowlisted Video.js/VHS +boundary for the default player. Shaka remains the only web source engine that +copies an arbitrary message and serialized `error.data` into a diagnostic. + +The locked workspace installs Shaka Player `5.2.2`. The audit used the +installed package's public declarations, `shaka.util.Error` JSDoc, compiled +runtime exports, and the load/error paths in the installed source. The public +runtime reports: + +- `shaka.Player.version === "v5.2.2"`; +- severity values `RECOVERABLE=1` and `CRITICAL=2`; +- category values `NETWORK=1`, `TEXT=2`, `MEDIA=3`, `MANIFEST=4`, + `STREAMING=5`, `DRM=6`, `PLAYER=7`, `CAST=8`, `STORAGE=9`, and `ADS=10`; +- numeric `shaka.util.Error.Code` values documented by the installed public + API. + +The existing `ShakaVideoSession` binds the public `Player#error` event before +calling `load()`, and also observes the public `load()` rejection. Those paths +have different terminal meaning and must not be collapsed into a +severity-only gate. + +## Goals + +- Convert Shaka errors into a minimal allowlisted `ShakaPlaybackEvidence` + value before classification or UI rendering. +- Retain only validated public severity, category, and code values, an + explicit lifecycle disposition, a code-proven stage/failure kind, and a + documented HTTP status. +- Classify only exact Shaka 5.2.2 category/code pairs. +- Keep insufficient or inconsistent evidence unknown. +- Keep recoverable player events from becoming terminal IPTVnator + diagnostics. +- Treat a rejected `Player.load()` as terminal even when the final public + Shaka error still carries `RECOVERABLE`. +- Prevent messages, URLs, headers, bodies, license/key payloads, credentials, + and arbitrary objects from reaching stored or rendered diagnostics. +- Version-lock the allowlist and tests to the installed Shaka public API. + +## Non-goals + +- Changing hls.js or Video.js/VHS evidence contracts. +- Redesigning mpegts.js diagnostics. +- Adding top-level diagnostic codes, history, persistence, correlation, + probes, automatic engine failover, or a cross-player recommendation matrix. +- Inspecting Shaka private loaders, networking state, parser internals, or + retry counters. +- Changing VOD multi-source auto-failover. +- Inferring CORS, codec, DRM, container, or stage from message text. + +## Approaches Considered + +### Public error boundary with lifecycle disposition (selected) + +Normalize the public `severity`, `category`, `code`, and only the documented +HTTP-status slot into `ShakaPlaybackEvidence`. The session supplies +`recoverable` for a recoverable event and `terminal` for a critical event or a +rejected load. Classification and technical details accept only the sanitized +evidence. + +This preserves the strongest stable facts while making unsafe fields +structurally unavailable to downstream code. It also represents the important +case where Shaka's final network error remains severity-recoverable but the +load promise has terminated. + +### Severity-only event and rejection gate + +Suppressing every `severity=RECOVERABLE` error would hide a terminal manifest +load failure after Shaka exhausts retries. Treating every error as terminal +would tear down a player that Shaka is still recovering. The route that +delivers the error is therefore required evidence. + +### Inspect request/parser internals + +Private networking and parser state could reveal request types and richer +stages, but it also exposes URLs, headers, response data, license payloads, and +unstable implementation details. This is rejected by the privacy and public +contract requirements. + +### Stop after the audit + +Stopping would be correct if the public API could not distinguish safe facts +or terminal lifecycle. The installed API exposes exact enums, documented +status layouts, and distinct event/rejection semantics, so a stable focused +improvement is available. + +## Installed Runtime And Event Ordering + +Shaka 5.2.2 documents recoverable severity as an error from which the player +is attempting to recover. It explicitly warns that some media-segment retry +paths may never escalate to a critical error. + +The installed runtime orders errors as follows: + +1. `PreloadManager.onError()` rejects its success promise for a critical + error, destroys preload work, and synchronously dispatches the public + `error` event. +2. The existing session's event listener therefore observes that critical + event before the `await player.load()` continuation handles the rejection. +3. Session teardown changes the current-player identity; the later rejection + is ignored, preventing a duplicate diagnostic. +4. Direct manifest/parser failures can reject the preload/load promise without + dispatching a player error. The load-rejection path must classify them. +5. The networking engine retries errors while their public severity is + recoverable. When attempts are exhausted it throws the last error without + rewriting its severity. A rejected `load()` is therefore terminal by + lifecycle even if evidence says `severity=recoverable`. +6. A recoverable public player event is not terminal and must leave the + current engine attached. + +`LOAD_INTERRUPTED` is suppressed only when the exact public triple is +`CRITICAL + PLAYER + LOAD_INTERRUPTED`; an arbitrary object containing the +number `7000` is not enough. + +## Version-locked Public Contract + +The production allowlist contains the exact Shaka 5.2.2 severity/category +values and the public online-playback error codes used by the boundary. A +contract test loads the installed compiled package in a child Node process, +asserts `v5.2.2`, compares the full severity/category maps, and verifies every +allowlisted code name/value against `shaka.util.Error.Code`. + +The explicit version assertion makes a dependency upgrade fail before new or +changed codes are silently accepted. An upgrade requires reviewing the new +public JSDoc layouts and lifecycle before updating the lock. + +## Evidence Contract + +`ShakaPlaybackEvidence` contains: + +- `severity`: `recoverable`, `critical`, or `unknown`; +- `category`: one validated public category name in lowercase, otherwise + `unknown`; +- `engineCode`: one allowlisted public numeric code, otherwise `unknown`; +- `disposition`: `terminal` or `recoverable`, supplied by the session route; +- `stage`: `manifest`, `segment`, `media`, `license`, or `unknown`; +- `failure`: `network`, `drm`, `manifest`, `media`, or `unknown`; +- optional `httpStatus`: an integer from 100 through 599. + +The boundary does not retain: + +- `error.message`; +- arbitrary or serialized `error.data`; +- request, redirect, manifest, segment, license, or certificate URLs; +- request or response headers; +- response text, bodies, or binary data; +- browser exceptions and events; +- DRM session metadata, keys, licenses, provider payloads, or credentials; +- unknown object properties. + +The existing `PlaybackDiagnostic.sourceUrl` remains the active source metadata +used by Retry, Copy URL, explicit external-player actions, and the existing VOD +failover flow. No URL is copied from a Shaka error. + +## HTTP Status Extraction + +Shaka 5.2.2 publicly documents: + +- `BAD_HTTP_STATUS` (`NETWORK`, code `1001`): `error.data[1]` is the status; +- `LICENSE_REQUEST_FAILED` (`DRM`, code `6007`): `error.data[0]` is a nested + Shaka networking error; +- `SERVER_CERTIFICATE_REQUEST_FAILED` (`DRM`, code `6017`): + `error.data[0]` is a nested Shaka networking error. + +The boundary reads a status only from an exact `NETWORK + BAD_HTTP_STATUS` +shape, directly or through those two exact documented nested-error layouts. +It validates the integer protocol range and copies only the number. Status-like +values in any other data position or arbitrary object are ignored. + +## Failure And Stage Mapping + +Failure uses exact category/code pairs: + +- allowlisted online network codes in the `NETWORK` category → `network`; +- allowlisted DRM codes in the `DRM` category → `drm`; +- exact manifest encryption/key-system codes → `drm`; +- exact media codes in the `MEDIA` category → `media`; +- exact manifest/parsing codes in the `MANIFEST` category → `manifest`; +- `DASH_UNSUPPORTED_CONTAINER` and + `CONTENT_UNSUPPORTED_BY_BROWSER` → `media`, because the public descriptions + prove a media support failure but the latter does not distinguish container + from codec; +- inconsistent category/code pairs, internal/test-only ambiguity, and + `RESTRICTIONS_CANNOT_BE_MET` → `unknown`. + +Stage is narrower than failure: + +- exact manifest codes → `manifest`; +- exact segment/index/init parsing codes and `SEGMENT_MISSING` → `segment`; +- exact MediaSource/video pipeline codes → `media`; +- exact license request/response/server-selection/expiry codes → `license`; +- everything else → `unknown`. + +Messages and arbitrary data never affect either value. + +## User-facing Classification + +No new top-level diagnostic code is needed: + +1. `failure=network` → `network-error`. +2. `failure=drm` → `drm-or-encryption`. +3. Exact `DASH_UNSUPPORTED_CONTAINER` → + `unsupported-container`. +4. Exact MediaSource operation or video-element failure codes that identify + the media pipeline → `media-decode-error`. +5. Manifest parsing, generic media parsing/transformation, ambiguous + browser-content support, restrictions, unknown values, and inconsistent + pairs → `unknown-playback-error`. + +Shaka 5.2.2 has no exact public code that distinguishes an unsupported codec +from an unsupported container for the generic +`CONTENT_UNSUPPORTED_BY_BROWSER` case. It therefore remains unknown instead of +becoming a false codec or container diagnosis. No Shaka path emits +`browser-access-error` without a structured public access code. + +## Session And Adapter Flow + +`ShakaVideoSession` owns disposition: + +- module-load rejection, unsupported browser capability, and `load()` + rejection are terminal; +- a public error event is terminal only for the exact critical severity; +- a recoverable event is passed through the shared evidence/classifier gate, + returns no diagnostic, and leaves the player attached; +- an event with unknown severity is ignored because terminal state is not + proven; +- a terminal diagnostic tears down the engine once; +- ClearKey sources keep the existing rule that external fallback is + unavailable because the external player never receives key configuration. + +HTML5 and ArtPlayer keep their current Shaka session adapters. The structured +diagnostic travels through their existing `emitPlaybackIssue` callbacks; no +player-selection, source-selection, or VOD failover behavior changes. + +Unsupported playlist DRM remains a pre-engine app diagnostic, but its +technical detail becomes a fixed safe description instead of echoing the +provider-supplied license string. + +## User Interface + +The existing technical “Error details” row renders only +`ShakaPlaybackEvidence`, for example: + +`stage=manifest · failure=network · severity=recoverable · category=network · code=1001 · disposition=terminal · HTTP 503` + +When structured Shaka evidence exists, legacy `details` and native message +fields are ignored even if a caller accidentally supplies them. Existing +titles, descriptions, HTTP badge, Retry, Copy URL, explicit MPV/VLC actions, +and layout remain unchanged. No translation or visual styling change is +required. + +## Testing + +Use test-driven development: + +- Compare the allowlist/version with the real installed Shaka 5.2.2 runtime. +- Exercise real documented direct and nested HTTP error shapes. +- Prove all unsafe data fields and misleading messages are excluded. +- Cover exact network, DRM, manifest, container, media, restrictions, + mismatched, and unknown classification. +- Prove recoverable classification returns no terminal diagnostic. +- Prove a recoverable-severity load rejection is terminal by lifecycle. +- Prove recoverable and unknown-severity events leave the session running. +- Prove a critical event during an in-flight load emits once and teardown does + not duplicate the later rejection. +- Cover ClearKey fallback suppression and ArtPlayer adapter routing. +- Prove the rendered detail row uses only sanitized Shaka evidence. + +Run the complete `ui-playback` unit target, its lint target, web typecheck, +i18n validation, release-note validation, and the repository test-impact pass. +No E2E is required because the player workflow, controls, source routing, and +integration lifecycle are unchanged; the event/rejection semantics are +covered at the session and adapter boundaries. + +## Documentation And Release Note + +Update `docs/architecture/embedded-inline-playback.md` as the canonical +browser diagnostic contract and the Shaka engine section in +`docs/architecture/m3u-playlist-module.md`. Keep the Shaka summaries in +`CLAUDE.md` and `AGENTS.md` current. Add a `fix(playback)` release note because +users receive safer technical details and more accurate Shaka diagnoses.