docs(playback): design structured Shaka diagnostics

This commit is contained in:
4gray committed 2026-07-31 09:36:44 +02:00
1 parent 9a50e7385b
commit d0694a33c4
2 files changed
+1000

No files matched your search

@@ -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<Record<string, number>>;
readonly category: Readonly<Record<string, number>>;
readonly code: Readonly<Record<string, number>>;
}
```
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<ShakaErrorLike> | 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.
@@ -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.