fix(playback): preserve Shaka text error evidence

This commit is contained in:
4gray committed 2026-07-31 20:50:59 +02:00
1 parent 50ca78d3a8
commit 8a5b6e85cc
8 files changed
+97 -13

No files matched your search

@@ -3,4 +3,4 @@ type: fix
area: playback
---
Shaka playback errors now use exact engine evidence, preserve external-player fallback when browser prerequisites are missing, keep recoverable retries non-terminal, and omit provider URLs, credentials, and response data from technical details.
Shaka playback errors now use exact engine evidence, including subtitle parsing, preserve external-player fallback when browser prerequisites are missing, keep recoverable retries non-terminal, and omit provider URLs, credentials, and response data from technical details.
+5 -4
View File
@@ -212,10 +212,11 @@ Key files:
`Channel.drm`, and the shared bridge exposes Shaka audio/text tracks via
source kind `shaka`. The Shaka `5.2.2` diagnostic boundary version-locks
public severity/category/code evidence, ignores recoverable error events,
treats rejected loads as terminal lifecycle outcomes, and never retains or
renders raw messages or `error.data`. A failed browser-support preflight
stays unknown but keeps external fallback for clear DASH; KODIPROP DRM still
suppresses it. See the CLAUDE.md "Video Players" feature entry and
treats rejected loads as terminal lifecycle outcomes, preserves exact public
DASH text-parser category/code evidence with unknown stage/failure, and never
retains or renders raw messages or `error.data`. A failed browser-support
preflight stays unknown but keeps external fallback for clear DASH; KODIPROP
DRM still suppresses it. See the CLAUDE.md "Video Players" feature entry and
`docs/architecture/m3u-playlist-module.md` ("DASH + ClearKey Playback").
- The built-in HTML5/hls.js player is the second guarded consumer.
`HtmlVideoPlayerComponent` provides a component-scoped
+4 -3
View File
@@ -737,9 +737,10 @@ app as a real argument, so it is not an option.
`libs/ui/playback/src/lib/shaka-engine/`. Its Shaka `5.2.2` diagnostic
boundary version-locks public severity/category/code evidence, ignores
recoverable error events, treats rejected loads as terminal lifecycle
outcomes, and never retains or renders raw messages or `error.data`. A failed
browser-support preflight stays unknown but keeps external fallback for clear
DASH; KODIPROP DRM still suppresses it. Details in
outcomes, preserves exact public DASH text-parser category/code evidence with
unknown stage/failure, and never retains or renders raw messages or
`error.data`. A failed browser-support preflight stays unknown but keeps
external fallback for clear DASH; KODIPROP DRM still suppresses it. Details in
`docs/architecture/m3u-playlist-module.md` ("DASH + ClearKey Playback").
- External players: MPV, VLC (via IPC to Electron backend)
- Embedded MPV (experimental, macOS/Windows/Linux): renders mpv video inside the Electron window through a native addon. macOS uses the libmpv render API in an `NSOpenGLView`; Windows uses in-process libmpv with `--wid` against an app-owned child `HWND`; Linux spawns an out-of-process `mpv --wid=<x11-window>` controlled over a JSON IPC socket (X11/XWayland only, requires system `mpv` on PATH; subtitles/speed/aspect/recording are not exported there). mpv's own screensaver inhibition does not apply to any of these paths, so `EmbeddedMpvNativeService` holds an Electron `powerSaveBlocker` (`prevent-display-sleep`) whenever any session's status is `playing`, and releases it on pause, dispose, or shutdown. Renderer bounds are CSS pixels; the service converts them to native units in the main process (`embedded-mpv-bounds.util.ts`: × page zoom everywhere, × display scale on Windows/Linux whose child windows are positioned in physical pixels; frame-copy bounds stay unscaled), and the session controller re-syncs bounds when `devicePixelRatio` changes. Service: `apps/electron-backend/src/app/services/embedded-mpv-native.service.ts`; full architecture: `docs/architecture/embedded-mpv-native.md`.
@@ -412,7 +412,9 @@ category alone is not container incompatibility, and Shaka messages never infer
CORS, codec, DRM, container, or stage. The public critical
`STREAMING_ENGINE_STARTUP_INVALID_STATE` code remains exact evidence while its
stage and failure stay unknown because the code does not identify a user-facing
media cause.
media cause. Public DASH text-parser codes are also retained exactly; their
`TEXT` category proves the parser subsystem, but not a safe manifest, segment,
or media cause, so stage and failure remain unknown.
A failed public `Player.isBrowserSupported()` preflight is not a Shaka error
and therefore retains fully unknown technical evidence instead of being
+6 -4
View File
@@ -1004,10 +1004,12 @@ player in settings.
validated HTTP status. Exact category/code pairs classify failures, while an
ambiguous Manifest category or unknown pair remains an unknown diagnostic.
The public streaming-startup code `5006` is retained exactly but keeps
unknown stage/failure. A failed `Player.isBrowserSupported()` preflight also
stays unknown rather than claiming container incompatibility; clear DASH
still offers configured external-player actions, while KODIPROP DRM keeps
them disabled because those players never receive its keys.
unknown stage/failure. Public DASH text-parser codes likewise retain their
exact `TEXT` category/code but keep stage/failure unknown. A failed
`Player.isBrowserSupported()` preflight also stays unknown rather than
claiming container incompatibility; clear DASH still offers configured
external-player actions, while KODIPROP DRM keeps them disabled because
those players never receive its keys.
Channels with `drm.supported === false` emit a `DrmOrEncryption` diagnostic
with a fixed safe detail string and without starting an engine.
- HTML5 player: `extension === 'mpd'` branch in `playChannel()`. ArtPlayer:
@@ -38,6 +38,16 @@ export const SHAKA_ERROR_CODE = {
REQUEST_FILTER_ERROR: 1006,
RESPONSE_FILTER_ERROR: 1007,
SEGMENT_MISSING: 1011,
// Text parsing reachable from DASH playback. HLS-only and app-uninvoked
// external-track/src= codes stay outside this boundary.
INVALID_TEXT_HEADER: 2000,
INVALID_TEXT_CUE: 2001,
UNABLE_TO_DETECT_ENCODING: 2003,
BAD_ENCODING: 2004,
INVALID_XML: 2005,
INVALID_MP4_TTML: 2007,
INVALID_MP4_VTT: 2008,
INVALID_MP4_CEA: 2010,
BUFFER_READ_OUT_OF_BOUNDS: 3000,
JS_INTEGER_OVERFLOW: 3001,
EBML_OVERFLOW: 3002,
@@ -210,6 +210,41 @@ describe('Shaka playback evidence', () => {
});
});
it.each([
['INVALID_TEXT_HEADER', 2000],
['INVALID_TEXT_CUE', 2001],
['UNABLE_TO_DETECT_ENCODING', 2003],
['BAD_ENCODING', 2004],
['INVALID_XML', 2005],
['INVALID_MP4_TTML', 2007],
['INVALID_MP4_VTT', 2008],
['INVALID_MP4_CEA', 2010],
])('preserves the exact public DASH text code %s', (_name, code) => {
expect(
createEvidence({ severity: 2, category: 2, code }, 'terminal')
).toEqual({
severity: 'critical',
category: 'text',
engineCode: code,
disposition: 'terminal',
stage: 'unknown',
failure: 'unknown',
});
});
it('rejects public text codes from app-uninvoked explicit-track APIs', () => {
expect(
createEvidence({ severity: 2, category: 2, code: 2014 }, 'terminal')
).toEqual({
severity: 'critical',
category: 'text',
engineCode: 'unknown',
disposition: 'terminal',
stage: 'unknown',
failure: 'unknown',
});
});
it.each([
{
label: 'manifest parsing',
@@ -286,6 +286,39 @@ describe('ShakaVideoSession', () => {
});
});
it('preserves a terminal text-parser event without retaining parser data', async () => {
const secret = 'subtitle-parser-secret';
const environment = createFakeShakaEnvironment();
const session = createSession(environment);
session.start(video, 'http://example.com/text-track.mpd');
await flush();
const player = environment.instances[0];
player.dispatch('error', {
severity: 2,
category: 2,
code: 2000,
message: `malformed WebVTT from ${secret}`,
data: [{ cue: secret }],
});
expect(issues).toHaveLength(1);
expect(issues[0].code).toBe(
PlaybackDiagnosticCode.UnknownPlaybackError
);
expect(issues[0].shaka).toEqual({
severity: 'critical',
category: 'text',
engineCode: 2000,
disposition: 'terminal',
stage: 'unknown',
failure: 'unknown',
});
expect(JSON.stringify(issues[0])).not.toContain(secret);
expect(player.destroyCount).toBe(1);
expect(session.getPlayer()).toBeNull();
});
it('emits a critical in-flight error once before teardown interrupts load', async () => {
const environment = createFakeShakaEnvironment({
onCreate: (player) => {