From 8a5b6e85ccb5c22e0084c1ea15a6b29e29f61b4c Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 31 Jul 2026 20:50:59 +0200 Subject: [PATCH] fix(playback): preserve Shaka text error evidence --- .../playback-structured-shaka-diagnostics.md | 2 +- AGENTS.md | 9 ++--- CLAUDE.md | 7 ++-- docs/architecture/embedded-inline-playback.md | 4 ++- docs/architecture/m3u-playlist-module.md | 10 +++--- .../lib/shaka-engine/shaka-error-contract.ts | 10 ++++++ .../shaka-playback-evidence.util.spec.ts | 35 +++++++++++++++++++ .../shaka-engine/shaka-video-session.spec.ts | 33 +++++++++++++++++ 8 files changed, 97 insertions(+), 13 deletions(-) diff --git a/.changes/playback-structured-shaka-diagnostics.md b/.changes/playback-structured-shaka-diagnostics.md index cb77c4e93..b6336b0f4 100644 --- a/.changes/playback-structured-shaka-diagnostics.md +++ b/.changes/playback-structured-shaka-diagnostics.md @@ -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. diff --git a/AGENTS.md b/AGENTS.md index f9ae7a08b..05878a128 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 5abf43553..a01743a2e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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=` 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`. diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index f13825e76..395a7349a 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.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 diff --git a/docs/architecture/m3u-playlist-module.md b/docs/architecture/m3u-playlist-module.md index 5f732a73b..cec501b91 100644 --- a/docs/architecture/m3u-playlist-module.md +++ b/docs/architecture/m3u-playlist-module.md @@ -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: diff --git a/libs/ui/playback/src/lib/shaka-engine/shaka-error-contract.ts b/libs/ui/playback/src/lib/shaka-engine/shaka-error-contract.ts index 4cd9ca143..78d0f09ed 100644 --- a/libs/ui/playback/src/lib/shaka-engine/shaka-error-contract.ts +++ b/libs/ui/playback/src/lib/shaka-engine/shaka-error-contract.ts @@ -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, diff --git a/libs/ui/playback/src/lib/shaka-engine/shaka-playback-evidence.util.spec.ts b/libs/ui/playback/src/lib/shaka-engine/shaka-playback-evidence.util.spec.ts index 59586066e..8696866b0 100644 --- a/libs/ui/playback/src/lib/shaka-engine/shaka-playback-evidence.util.spec.ts +++ b/libs/ui/playback/src/lib/shaka-engine/shaka-playback-evidence.util.spec.ts @@ -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', diff --git a/libs/ui/playback/src/lib/shaka-engine/shaka-video-session.spec.ts b/libs/ui/playback/src/lib/shaka-engine/shaka-video-session.spec.ts index 7eb3dcaab..bcdc7f5cc 100644 --- a/libs/ui/playback/src/lib/shaka-engine/shaka-video-session.spec.ts +++ b/libs/ui/playback/src/lib/shaka-engine/shaka-video-session.spec.ts @@ -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) => {