Files
iptvnator/docs/superpowers/plans/2026-07-31-structured-videojs-vhs-diagnostics.md
T

15 KiB

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

  • Step 1: Install locked dependencies

Run:

pnpm install --frozen-lockfile

Expected: exit 0 without changing pnpm-lock.yaml.

  • 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: 85 suites and 765 tests pass before implementation.

  • Step 4: Audit exact installed and upstream versions

Confirm:

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:

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:

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:

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

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:

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:

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:

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:

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:

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:

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:

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

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:

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

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:

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:

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.