diff --git a/docs/superpowers/plans/2026-07-31-structured-videojs-vhs-diagnostics.md b/docs/superpowers/plans/2026-07-31-structured-videojs-vhs-diagnostics.md new file mode 100644 index 000000000..a65336b01 --- /dev/null +++ b/docs/superpowers/plans/2026-07-31-structured-videojs-vhs-diagnostics.md @@ -0,0 +1,518 @@ +# Structured Video.js/VHS 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:** Add a safe, exact-value Video.js/VHS evidence contract for the +default web player without depending on private VHS internals. + +**Architecture:** Read the public Video.js `MediaError` inside the existing +`Player#error` listener, detect active VHS through the documented +`player.tech().vhs` runtime property, and sanitize the error into a small +allowlisted evidence value. Classify only confirmed public values, keep +generic non-VHS Video.js errors on the native path, and render only the +sanitized evidence. + +**Tech Stack:** Angular 21, TypeScript 5.9, Video.js 8.23.9, VHS 3.17.5, 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/.pnpm/video.js@8.23.9/node_modules/video.js/dist/types/media-error.d.ts` +- Verify: + `node_modules/.pnpm/@videojs+http-streaming@3.17.5_video.js@8.23.9/node_modules/@videojs/http-streaming/README.md` +- Verify: + `node_modules/.pnpm/@videojs+http-streaming@3.17.5_video.js@8.23.9/node_modules/@videojs/http-streaming/src/videojs-http-streaming.js` + +- [x] **Step 1: Install locked dependencies** + +Run: + +```bash +pnpm install --frozen-lockfile +``` + +Expected: exit 0 without changing `pnpm-lock.yaml`. + +- [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: 85 suites and 765 tests pass before implementation. + +- [x] **Step 4: Audit exact installed and upstream versions** + +Confirm: + +```text +video.js = 8.23.9 +@videojs/http-streaming = 3.17.5 +Video.js v8.23.9 tag = 81b3cb429fae8dd00659ac5d3b0b1d2d20a283cb +VHS v3.17.5 tag = a9f9d7ac0264b373f14da1bb2f2e7fe8f2775c4f +``` + +Read the public Video.js `MediaError` and player error API, the VHS README +runtime properties/events, and the tagged upstream error/recovery tests. +Reject private request/loaders and undocumented retry/exclusion events from +the production design. + +### Task 1: Drive The VHS Evidence Boundary From Failing Tests + +**Files:** +- Create: + `libs/ui/playback/src/lib/playback-diagnostics/vhs-playback-evidence.util.spec.ts` +- Create: + `libs/ui/playback/src/lib/playback-diagnostics/vhs-playback-evidence.util.ts` +- Modify: + `libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.model.ts` +- Modify: + `libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.ts` +- Modify: + `libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.spec.ts` + +- [ ] **Step 1: Write the failing allowlist and sanitizer tests** + +In `vhs-playback-evidence.util.spec.ts`, import the actual installed Video.js +runtime and the wished-for boundary: + +```typescript +import videoJs from 'video.js'; +import { + VhsPlaybackEngineType, + createVhsPlaybackEvidence, +} from './vhs-playback-evidence.util'; + +it('matches the installed public videojs.Error identifiers', () => { + expect(Object.values(VhsPlaybackEngineType).sort()).toEqual( + Object.values(videoJs.Error).sort() + ); +}); +``` + +Add installed-runtime-shaped inputs for: + +```typescript +const unsafeError = { + code: 4, + status: 503, + message: + 'HLS playlist request error at URL: ' + + 'https://provider.example/live.m3u8?token=secret', + metadata: { + errorType: videoJs.Error.NetworkBadStatus, + requestType: 'hls-playlist', + uri: 'https://provider.example/live.m3u8?token=secret', + headers: { Authorization: 'Bearer secret' }, + responseText: 'provider body secret', + }, +}; +``` + +Expect only: + +```typescript +{ + engineType: 'networkbadstatus', + mediaErrorCode: 4, + disposition: 'terminal', + stage: 'unknown', + httpStatus: 503, +} +``` + +Serialize the evidence and prove it contains none of the URL, token, header, +body, message, request type, or arbitrary metadata sentinels. + +Cover: + +- every public `videojs.Error` value; +- unknown and malformed error types; +- standard code boundaries 0/5 and invalid values; +- HTTP boundaries 399/400/599/600 and non-integers; +- exact HLS playlist, DASH manifest, and segment-operation stage mappings. + +- [ ] **Step 2: Write failing classifier tests** + +In `playback-diagnostics.util.spec.ts`, add wished-for +`classifyVhsPlaybackIssue` cases: + +```typescript +expect(classifyVhsPlaybackIssue(networkError, metadata)).toEqual( + expect.objectContaining({ + code: PlaybackDiagnosticCode.NetworkError, + source: PlaybackDiagnosticSource.Vhs, + httpStatus: 503, + externalFallbackRecommended: false, + }) +); +``` + +Add regressions proving: + +- exact network types classify as `network-error` without message matching; +- exact `streamingfailedtodecryptsegment` classifies as + `drm-or-encryption`; +- standard code 5 classifies as `drm-or-encryption`; +- generic VHS code 3 stays `unknown-playback-error`; +- unknown provider values and misleading messages stay unknown; +- the diagnostic contains no `nativeErrorMessage`; +- the top-level status and structured evidence are retained. + +Keep the existing generic native code-3 expectation unchanged. + +- [ ] **Step 3: Run the focused tests to verify RED** + +Run: + +```bash +NODE_OPTIONS=--experimental-vm-modules \ +node node_modules/jest/bin/jest.js \ + --config jest.web-esm.workspace.ts \ + --runTestsByPath \ + libs/ui/playback/src/lib/playback-diagnostics/vhs-playback-evidence.util.spec.ts \ + libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.spec.ts \ + --runInBand +``` + +Expected: FAIL because the VHS evidence model, extractor, source, and +classifier do not exist. + +- [ ] **Step 4: Add the minimal evidence model** + +In `playback-diagnostics.model.ts`, add const objects and extracted types: + +```typescript +export const VhsPlaybackDisposition = { + Terminal: 'terminal', +} as const; + +export const VhsPlaybackStage = { + Manifest: 'manifest', + Playlist: 'playlist', + Segment: 'segment', + Unknown: 'unknown', +} as const; + +export const VhsPlaybackMediaErrorCode = { + Custom: 0, + Aborted: 1, + Network: 2, + Decode: 3, + SourceNotSupported: 4, + Encrypted: 5, + Unknown: 'unknown', +} as const; +``` + +Define `VhsPlaybackEngineType`, including an `unknown` fallback, and +`VhsPlaybackEvidence` with engine type, validated media error code, terminal +disposition, stage, and optional status. Add: + +```typescript +readonly vhs?: VhsPlaybackEvidence; +``` + +to `PlaybackDiagnostic`, and add `Vhs: 'vhs'` to +`PlaybackDiagnosticSource`. + +- [ ] **Step 5: Implement the allowlisted extractor** + +In `vhs-playback-evidence.util.ts`: + +- define one const object containing the exact installed public + `videojs.Error` strings; +- validate `metadata.errorType` through a readonly set; +- validate only standard error codes 0 through 5; +- validate only integer HTTP status 400 through 599; +- map only exact public parser/segment identifiers to stages; +- return a fresh object containing only the evidence fields; +- never read the message or any metadata key other than `errorType`. + +- [ ] **Step 6: Implement exact classification** + +In `playback-diagnostics.util.ts`, add: + +```typescript +export function classifyVhsPlaybackIssue( + error: NativePlaybackErrorInput, + metadata: PlaybackSourceMetadata +): PlaybackDiagnostic +``` + +Create evidence once, then classify by validated HTTP status, exact network +types, standard network code, exact decrypt type, standard encrypted code, or +known unsupported container. Keep all remaining evidence unknown. Store the +evidence on the diagnostic, copy its status into the existing `httpStatus`, +copy its validated code into `nativeErrorCode`, and do not copy the error +message or arbitrary metadata. + +- [ ] **Step 7: Run focused tests to verify GREEN** + +Run the command from step 3. + +Expected: PASS for allowlisting, privacy, exact classification, code-3 +unknown behavior, and unchanged native tests. + +### Task 2: Route Only Active VHS Errors Through The Boundary + +**Files:** +- Modify: + `libs/ui/playback/src/lib/vjs-player/vjs-player.types.ts` +- Modify: + `libs/ui/playback/src/lib/vjs-player/vjs-player.types.spec.ts` +- Modify: + `libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts` +- Modify: + `libs/ui/playback/src/lib/vjs-player/vjs-player.component.spec.ts` + +- [ ] **Step 1: Write failing active-VHS detection tests** + +In `vjs-player.types.spec.ts`, add expectations for a wished-for +`hasActiveVhsSourceHandler` helper: + +```typescript +expect(hasActiveVhsSourceHandler(playerWithVhs)).toBe(true); +expect(hasActiveVhsSourceHandler(playerWithoutVhs)).toBe(false); +expect(hasActiveVhsSourceHandler(throwingPlayer)).toBe(false); +``` + +The helper may inspect only the documented `player.tech().vhs` property. + +- [ ] **Step 2: Write failing component routing regressions** + +Extend the player harness with an optional `vhs` object on the current tech. +Add tests proving: + +- active VHS + real network error shape emits a structured VHS network + diagnostic; +- the unsafe VHS message and metadata are absent; +- active VHS + generic code 3 emits unknown; +- no VHS + native code 3 retains `media-decode-error`; +- a populated `player.error()` still wins over `video.error`. + +- [ ] **Step 3: Run the focused component tests to verify RED** + +Run: + +```bash +NODE_OPTIONS=--experimental-vm-modules \ +node node_modules/jest/bin/jest.js \ + --config jest.web-esm.workspace.ts \ + --runTestsByPath \ + libs/ui/playback/src/lib/vjs-player/vjs-player.types.spec.ts \ + libs/ui/playback/src/lib/vjs-player/vjs-player.component.spec.ts \ + --runInBand +``` + +Expected: FAIL because active VHS detection and routing do not exist. + +- [ ] **Step 4: Implement the public runtime check and routing** + +Add a guarded helper in `vjs-player.types.ts` that returns true only when +`player.tech()?.vhs` is a non-null object. In +`VjsPlayerComponent.handleVideoJsError`, call +`classifyVhsPlaybackIssue` only when that helper is true and +`player.error()` returned an error. Otherwise keep +`classifyNativePlaybackIssue(playerError ?? video.error, metadata)`. + +Do not add xhr hooks, VHS loader access, retry listeners, or private fields. + +- [ ] **Step 5: Run the focused component tests to verify GREEN** + +Run the command from step 3. + +Expected: PASS. + +### Task 3: Render Only Structured VHS Details + +**Files:** +- Modify: + `libs/ui/playback/src/lib/web-player-view/web-player-view-diagnostics.utils.ts` +- Modify: + `libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts` + +- [ ] **Step 1: Write the failing safe rendering regression** + +Create a VHS diagnostic carrying sanitized evidence plus provider-secret +sentinels in fields that must not be rendered. Expect: + +```typescript +{ + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_ERROR_DETAILS', + value: + 'stage=unknown · type=networkbadstatus · code=4 · ' + + 'disposition=terminal · HTTP 503', +} +``` + +Prove the rendered details do not contain the provider URL, token, headers, +message, response body, or arbitrary metadata. + +- [ ] **Step 2: Run the focused view test to verify RED** + +Run: + +```bash +NODE_OPTIONS=--experimental-vm-modules \ +node node_modules/jest/bin/jest.js \ + --config jest.web-esm.workspace.ts \ + --runTestsByPath \ + libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts \ + --runInBand +``` + +Expected: FAIL because the formatter has no VHS evidence branch. + +- [ ] **Step 3: Implement deterministic VHS formatting** + +In `formatDiagnosticErrorDetails`, add a VHS branch before generic native +formatting. Build the summary only from evidence stage, engine type, media +error code, disposition, and optional status. Add `vhs` to the diagnostic +source formatter as `Video.js / VHS`. + +- [ ] **Step 4: Run the focused view test to verify GREEN** + +Run the command from step 2. + +Expected: PASS and the existing HLS summary remains unchanged. + +### Task 4: Document, Release-Note, And Validate + +**Files:** +- Modify: `docs/architecture/embedded-inline-playback.md` +- Create: `.changes/playback-structured-videojs-diagnostics.md` +- Verify: `AGENTS.md` +- Verify: `CLAUDE.md` + +- [ ] **Step 1: Update the canonical diagnostic contract** + +Document: + +- Video.js 8.23.9 / VHS 3.17.5 public evidence boundary; +- exact allowlist and conservative classification; +- safe stage mapping; +- terminal `Player#error` semantics and recoverable VHS suppression; +- rejected private request/loader fields and unsafe payloads; +- active-VHS code 3 remaining unknown. + +No `AGENTS.md` or `CLAUDE.md` change is expected because neither currently +describes this diagnostic boundary. Re-check both after the runtime diff. + +- [ ] **Step 2: Add the release note** + +Create: + +```markdown +--- +type: fix +area: playback +--- + +The default web player now reports safer, more accurate streaming errors: +confirmed network and encrypted-segment failures keep structured details, +while ambiguous Video.js errors remain unknown instead of suggesting the +wrong cause. +``` + +- [ ] **Step 3: Run targeted and affected validation** + +Run: + +```bash +pnpm nx test ui-playback +pnpm nx lint ui-playback +pnpm nx typecheck web +pnpm run i18n:validate +pnpm run release:notes:validate +``` + +If `web:typecheck` is not the actual target name, inspect +`pnpm nx show project web` and run the repository's declared typecheck target. + +Expected: all commands exit 0. + +- [ ] **Step 4: Complete the test-impact pass** + +Use: + +```bash +pnpm nx show projects --withTarget test +pnpm nx show projects --withTarget e2e +``` + +Record that `ui-playback` unit/component tests, lint, web typecheck, i18n, and +release-note validation cover the changed boundary. E2E is skipped unless the +implementation changes workflow, routing, playback lifecycle, or player +recovery behavior. + +### Task 5: Independent Review, Final Verification, And Ready PR + +**Files:** +- Review: complete diff from `origin/master...HEAD` + +- [ ] **Step 1: Run an independent local Codex review** + +Provide the reviewer with the user constraints, exact installed versions, and +the full diff. Ask only for actionable P0/P1/P2 correctness, privacy, public +API stability, event-ordering, regression, test, and scope findings. + +- [ ] **Step 2: Fix every valid P0/P1/P2 finding with TDD** + +For each finding, add or adjust a failing regression test first, verify RED, +apply the minimal fix, and verify GREEN. Reject incorrect findings with +specific source/test evidence. + +- [ ] **Step 3: Repeat the independent review** + +Run the same full-diff review again. Expected: no actionable P0/P1/P2 +findings. + +- [ ] **Step 4: Run fresh full validation** + +Repeat every command from Task 4 step 3 after the final review fix. Inspect +the complete output and confirm zero failures. + +- [ ] **Step 5: Inspect final scope and documentation** + +Run: + +```bash +git status --short +git diff --check origin/master...HEAD +git diff --stat origin/master...HEAD +git diff origin/master...HEAD +``` + +Confirm: + +- no hls.js contract changes unless a real regression required one; +- no Shaka/mpegts redesign; +- no private VHS production access; +- no unsafe error payload retention/rendering; +- docs and one release note are present; +- no unrelated files changed. + +- [ ] **Step 6: Commit, push, and create the ready PR** + +Use a conventional `fix(playback): ...` commit for runtime behavior. Push +`agent/structured-vhs-diagnostics` and create a non-draft PR with the evidence +summary, privacy boundary, tests, validation, E2E rationale, and review result. diff --git a/docs/superpowers/specs/2026-07-31-structured-videojs-vhs-diagnostics-design.md b/docs/superpowers/specs/2026-07-31-structured-videojs-vhs-diagnostics-design.md new file mode 100644 index 000000000..eedca5d03 --- /dev/null +++ b/docs/superpowers/specs/2026-07-31-structured-videojs-vhs-diagnostics-design.md @@ -0,0 +1,298 @@ +# Structured Video.js/VHS Diagnostics + +## Context + +PR #1314 made generic native playback diagnostics preserve Video.js HTTP +status and `metadata.errorType`, and stopped treating an ambiguous +`MediaError` code 4 as codec evidence. PR #1316 added a separate structured +hls.js boundary for the HTML5 and ArtPlayer engines. Video.js remains the +default web player, but its HLS/DASH implementation is VHS rather than hls.js, +so the hls.js evidence contract does not apply to it. + +The locked workspace installs: + +- Video.js `8.23.9`; +- bundled `@videojs/http-streaming` (VHS) `3.17.5`; +- an unrelated Video.js `7.21.7` / VHS `2.16.3` pair nested under the legacy + aspect-ratio plugin. + +The application imports the root Video.js `8.23.9` build. Its package declares +VHS `^3.17.5`, and pnpm resolves that dependency to `3.17.5`. + +The audit compared the installed sources with the exact upstream tags: + +- Video.js `v8.23.9`, commit + `81b3cb429fae8dd00659ac5d3b0b1d2d20a283cb`; +- VHS `v3.17.5`, commit + `a9f9d7ac0264b373f14da1bb2f2e7fe8f2775c4f`. + +The installed VHS `error-codes.js`, `videojs-http-streaming.js`, and +`playlist-controller.js` match the tagged sources. + +## Goals + +- Add a minimal allowlisted evidence boundary for terminal Video.js/VHS + errors. +- Retain only public Video.js `MediaError` fields and exact public + `videojs.Error` identifiers. +- Classify only exact confirmed values; unknown values remain unknown. +- Keep recoverable VHS playlist/segment handling from becoming a terminal + IPTVnator diagnostic. +- Avoid retaining or rendering VHS/provider URLs, headers, xhr objects, + response bodies, messages, credentials, or arbitrary metadata. +- Correct the misleading case where VHS promotes a generic internal object or + string to code 3 even though no media-decode cause was established. +- Keep generic native Video.js playback behavior unchanged when VHS is not the + active source handler. + +## Non-goals + +- Changing the hls.js evidence contract from PR #1316. +- A Shaka or mpegts.js diagnostic redesign. +- Inferring CORS, mixed content, CSP, private-network access, codec, DRM, or a + request stage from messages or indirect signals. +- Reading VHS playlist loaders, segment loaders, request objects, or other + private implementation state. +- Listening to undocumented VHS retry/exclusion events in production. +- Diagnostic history, persistence, correlation, stream probes, automatic + failover, or a cross-player recommendation matrix. + +## Approaches Considered + +### Public Video.js error boundary (selected) + +Read `player.error()` inside the public `Player#error` listener. When the +documented `player.tech().vhs` runtime property is present, sanitize the error +into a small `VhsPlaybackEvidence` object. Validate `metadata.errorType` +against the exact values exported by `videojs.Error`, validate the standard +`MediaError` code and HTTP status, and derive a stage only where the public +engine identifier itself names that stage. + +This approach adds structured evidence without depending on VHS loaders or +request objects. It also lets generic non-VHS Video.js errors continue through +the existing native classifier. + +### Observe VHS xhr hooks + +VHS documents `vhs.xhr.onRequest` and `onResponse`, but request hooks would +require correlating mutable request objects with a later terminal error. They +also expose URLs, headers, response objects, and provider payloads at exactly +the boundary that must stay sanitized. Request completion is not terminal +playback disposition: VHS may retry, exclude a rendition, or recover. This +approach is rejected. + +### Read VHS loaders and retry events + +The installed implementation carries precise `requestType`, xhr, playlist, +segment, and key context internally. Those objects and their event ordering +are not part of the documented stable error API. Depending on +`playlistController_`, loader error objects, `retryplaylist`, or +`excludeplaylist` would violate the scope constraint and make updates to VHS +risky. This approach is rejected. + +### Stop after the audit + +Stopping would be correct if the public API exposed nothing beyond PR #1314. +The audit found a safe increment: `videojs.Error` is a public exact-value +allowlist, `player.error()` is populated before `Player#error`, and active VHS +can be detected through a documented runtime property. That is enough to +structure evidence, suppress unsafe message/metadata retention, and avoid a +false generic code-3 decode diagnosis. + +## Stable Public Evidence + +Video.js `8.23.9` documents and types these public fields: + +- `player.error()` returns the current Video.js `MediaError`; +- `MediaError.code` carries standard codes 0 through 5; +- `MediaError.status` is an optional plugin-supplied status; +- `MediaError.metadata.errorType` is expected to align with + `videojs.Error`; +- `Player#error` is emitted after `player.error_` has been replaced with the + new `MediaError`; +- `player.tech().vhs` is a documented VHS runtime property while VHS is in + use. + +Video.js `8.23.9` publicly exports these exact `videojs.Error` values: + +- `networkbadstatus`; +- `networkrequestfailed`; +- `networkrequestaborted`; +- `networkrequesttimeout`; +- `networkbodyparserfailed`; +- `streaminghlsplaylistparsererror`; +- `streamingdashmanifestparsererror`; +- `streamingcontentsteeringparsererror`; +- `streamingvttparsererror`; +- `streamingfailedtoselectnextsegment`; +- `streamingfailedtodecryptsegment`; +- `streamingfailedtotransmuxsegment`; +- `streamingfailedtoappendsegment`; +- `streamingcodecschangeerror`. + +The allowlist is intentionally version-locked. A regression test compares it +with the installed `videojs.Error` export so a dependency update requires a +new audit instead of silently accepting new engine values. + +## Rejected Evidence + +VHS `3.17.5` internally adds fields such as `requestType`, `uri`, `headers`, +`error`, xhr objects, response text, playlist objects, and segment context. +Those values can contain credentials or provider data and are not required by +the public Video.js `ErrorMetadata` contract. The boundary must not read or +copy them. + +The following are also rejected: + +- error `message`, because VHS messages embed request URLs; +- `responseText`, response data, and response bodies; +- request/response headers; +- arbitrary metadata keys or provider objects; +- `requestType`, because its propagation through `player.error()` is an + implementation detail rather than a documented stable contract; +- status zero as CORS or browser-access evidence; +- message fragments as codec, DRM, network, access, or stage evidence. + +The existing `PlaybackDiagnostic.sourceUrl` remains available to the +pre-existing Retry, Copy URL, and explicit external-player workflows. No URL +from the Video.js/VHS error object is copied into evidence or technical +details. + +## Evidence Contract + +`VhsPlaybackEvidence` contains: + +- `engineType`: one exact installed `videojs.Error` value, otherwise + `unknown`; +- `mediaErrorCode`: a validated standard `MediaError` code 0 through 5, + otherwise `unknown`; +- `disposition`: `terminal`; +- `stage`: `manifest`, `playlist`, `segment`, or `unknown`; +- optional `httpStatus`: an integer from 400 through 599 copied only from the + public top-level `MediaError.status`. + +There is no recoverable evidence object. IPTVnator creates this boundary only +from the public `Player#error` event after Video.js has stored the final error. +Recoverable VHS handling remains inside VHS and produces no terminal +diagnostic. + +## Stage Mapping + +Stage is derived only from exact public engine identifiers: + +- `streaminghlsplaylistparsererror` → `playlist`; +- `streamingdashmanifestparsererror` → `manifest`; +- `streamingfailedtoselectnextsegment` → `segment`; +- `streamingfailedtodecryptsegment` → `segment`; +- `streamingfailedtotransmuxsegment` → `segment`; +- `streamingfailedtoappendsegment` → `segment`; +- all network identifiers, content-steering/VTT parser errors, codec-change + errors, and unrecognized values → `unknown`. + +Network errors remain stage-unknown even if internal metadata happens to carry +`requestType: hls-playlist`, `hls-segment`, or `hls-key`. The public error +contract does not guarantee those values. + +## Classification + +`classifyVhsPlaybackIssue` uses this precedence: + +1. A validated HTTP 4xx/5xx status produces `network-error`. +2. Exact public network error types produce `network-error`. +3. Standard `MediaError` code 2 produces `network-error`. +4. Exact `streamingfailedtodecryptsegment` produces + `drm-or-encryption`. +5. Standard `MediaError` code 5 produces `drm-or-encryption`. +6. A known browser-incompatible source container may still produce + `unsupported-container`. +7. Everything else produces `unknown-playback-error`. + +Generic VHS code 3 does not produce `media-decode-error`. VHS assigns code 3 +to object errors without a code and to string errors before calling +`player.error()`, including the terminal “no available playlists” path. +Therefore code 3 is not sufficient decode evidence on the VHS path. + +The boundary does not classify: + +- codec incompatibility from codec-change, transmux, append, or message text; +- DRM from key-request/load failure; +- browser access from status zero or generic request failure; +- media decode from code 3 alone. + +Generic Video.js playback without active VHS continues to use the existing +native classifier, so a native code 3 remains a media-decode diagnostic. + +## Runtime And Event Ordering + +VHS `3.17.5` handles playlist and segment failures before the public terminal +error: + +- a failed rendition can be excluded and another selected; +- a single finite-exclusion rendition is retried; +- previously excluded renditions can be re-included; +- segment timeouts can trigger ABR recovery; +- aborted segment requests are ignored as non-errors; +- only an unrecoverable playlist-controller error reaches + `player.error(...)`. + +Video.js `8.23.9` constructs and stores the `MediaError`, then synchronously +fires `Player#error`. The component therefore reads the final public error +inside its existing listener and marks it terminal. IPTVnator does not +subscribe to internal VHS recovery events. + +## Component Flow + +`VjsPlayerComponent` keeps one `Player#error` listener: + +1. Read `player.error()` before falling back to the native video error. +2. Detect active VHS through the documented `player.tech().vhs` runtime + property. +3. With active VHS and a Video.js error, call + `classifyVhsPlaybackIssue`. +4. Otherwise keep the existing `classifyNativePlaybackIssue` path. +5. Emit exactly one terminal diagnostic. + +The component does not inspect `playlistController_`, loaders, xhr hooks, +request types, retry events, or error messages. + +## User Interface + +Add one deterministic Video.js/VHS technical-detail summary: + +`stage=unknown · type=networkbadstatus · code=4 · disposition=terminal · HTTP 503` + +The summary is built only from `VhsPlaybackEvidence`. It never includes the +Video.js message or arbitrary metadata. Existing diagnostic headings, +descriptions, HTTP badge, Retry, Copy URL, and explicit MPV/VLC actions remain +unchanged; no new translation keys or layout changes are needed. + +## Testing + +Use test-driven development: + +- Compare the allowlist with the actual installed `videojs.Error` export. +- Exercise real Video.js public event ordering by setting an error on a real + Video.js player and reading `player.error()` from its `error` listener. +- Use installed-runtime-shaped VHS errors for 5xx, request failure, timeout, + playlist parsing, segment decrypt, generic code 3, and unknown provider + metadata. +- Prove URLs, headers, response data, messages, credentials, request types, + and arbitrary metadata do not survive the boundary or rendered details. +- Prove only exact public network/decrypt values affect classification. +- Prove a generic active-VHS code 3 stays unknown while non-VHS native code 3 + remains a media-decode diagnostic. +- Prove the component routes active VHS errors through the structured boundary + and leaves generic Video.js errors on the native path. +- Prove technical details render only the sanitized VHS evidence. + +Run the complete `ui-playback` unit target, its lint target, the web typecheck, +i18n validation, release-note validation, and the repository test-impact pass. +No E2E is required because the playback workflow, controls, routing, and +integration lifecycle are unchanged; the change is confined to the existing +terminal error boundary and technical details. + +## Documentation And Release Note + +Update `docs/architecture/embedded-inline-playback.md`, the canonical browser +playback diagnostic contract. Add a `fix(playback)` release note because users +receive more accurate default-player diagnoses and safer technical details.