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.Errorvalue; -
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-errorwithout message matching; - exact
streamingfailedtodecryptsegmentclassifies asdrm-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.Errorstrings; -
validate
metadata.errorTypethrough 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 overvideo.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#errorsemantics 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.