12 KiB
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/ VHS2.16.3pair 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, commit81b3cb429fae8dd00659ac5d3b0b1d2d20a283cb; - VHS
v3.17.5, commita9f9d7ac0264b373f14da1bb2f2e7fe8f2775c4f.
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
MediaErrorfields and exact publicvideojs.Erroridentifiers. - 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.jsMediaError;MediaError.codecarries standard codes 0 through 5;MediaError.statusis an optional plugin-supplied status;MediaError.metadata.errorTypeis expected to align withvideojs.Error;Player#erroris emitted afterplayer.error_has been replaced with the newMediaError;player.tech().vhsis 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 throughplayer.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 installedvideojs.Errorvalue, otherwiseunknown;mediaErrorCode: a validated standardMediaErrorcode 0 through 5, otherwiseunknown;disposition:terminal;stage:manifest,playlist,segment, orunknown;- optional
httpStatus: an integer from 400 through 599 copied only from the public top-levelMediaError.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:
- A validated HTTP 4xx/5xx status produces
network-error. - Exact public network error types produce
network-error. - Standard
MediaErrorcode 2 producesnetwork-error. - Exact
streamingfailedtodecryptsegmentproducesdrm-or-encryption. - Standard
MediaErrorcode 5 producesdrm-or-encryption. - A known browser-incompatible source container may still produce
unsupported-container. - 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:
- Read
player.error()before falling back to the native video error. - Detect active VHS through the documented
player.tech().vhsruntime property. - With active VHS and a Video.js error, call
classifyVhsPlaybackIssue. - Otherwise keep the existing
classifyNativePlaybackIssuepath. - 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.Errorexport. - Exercise real Video.js public event ordering by setting an error on a real
Video.js player and reading
player.error()from itserrorlistener. - 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.