Files
iptvnator/docs/superpowers/plans/2026-07-31-structured-shaka-diagnostics.md
T
4gray 9f4e11d6de fix(playback): structure Shaka diagnostics (#1318)
* docs(playback): design structured Shaka diagnostics

* fix(playback): structure Shaka diagnostics

* docs(playback): document Shaka evidence boundary

* docs(playback): fix Shaka validation commands

* fix(playback): preserve Shaka fallback evidence

* fix(playback): preserve Shaka text error evidence
2026-07-31 21:25:06 +02:00

20 KiB

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

  • Step 1: Install locked dependencies

Run:

pnpm install --frozen-lockfile

Expected: exit 0, Shaka Player 5.2.2, and no lockfile change.

  • Step 2: Verify Nx workspace discovery

Run:

pnpm nx show projects

Expected: exit 0 and output containing ui-playback, web, and web-e2e.

  • Step 3: Run the affected-project baseline

Run:

pnpm nx test ui-playback

Expected: 86 suites and 801 tests pass before implementation.

  • Step 4: Audit the installed public runtime

Confirm:

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:

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:

interface InstalledShakaContract {
    readonly version: string;
    readonly severity: Readonly<Record<string, number>>;
    readonly category: Readonly<Record<string, number>>;
    readonly code: Readonly<Record<string, number>>;
}

Assert:

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:

NODE_OPTIONS=--experimental-vm-modules node node_modules/jest/bin/jest.js \
  --config jest.web-esm.workspace.ts --runTestsByPath \
  libs/ui/playback/src/lib/shaka-engine/shaka-playback-evidence.util.spec.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:

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:

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:

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:

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:

{
    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:

export function createShakaPlaybackEvidence(
    error: Partial<ShakaErrorLike> | null | undefined,
    disposition: ShakaPlaybackDisposition
): ShakaPlaybackEvidence;

Use only:

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:

classifyShakaPlaybackIssue(
    error,
    metadata,
    disposition
): PlaybackDiagnostic | null;

Add expectations:

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:

const evidence = createShakaPlaybackEvidence(error, disposition);
if (evidence.disposition === ShakaPlaybackDisposition.Recoverable) {
    return null;
}

Select the top-level code only from exact evidence:

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:

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:

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:

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:

expect(issue.shaka).toEqual({
    severity: 'recoverable',
    category: 'network',
    engineCode: 1001,
    disposition: 'terminal',
    stage: 'unknown',
    failure: 'network',
    httpStatus: 503,
});

Run:

NODE_OPTIONS=--experimental-vm-modules node node_modules/jest/bin/jest.js \
  --config jest.web-esm.workspace.ts --runTestsByPath \
  libs/ui/playback/src/lib/shaka-engine/shaka-video-session.spec.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:

{
    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:

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: 'unknown',
    failure: 'network',
    httpStatus: 503,
},

Expect the error-details row to equal:

stage=unknown · 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:

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:

---
type: fix
area: playback
---

Shaka playback errors now use exact engine evidence, keep recoverable retries from becoming terminal diagnostics, and omit provider URLs, credentials, and response data from technical details.
  • Step 3: Run focused and affected validation

Run:

NODE_OPTIONS=--experimental-vm-modules node node_modules/jest/bin/jest.js \
  --config jest.web-esm.workspace.ts --runTestsByPath \
  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 \
  --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:

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:

git status --short
git log --oneline origin/master..HEAD
  • Step 2: Dispatch an independent local Codex review

Give the reviewer:

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:

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.