fix(playback): preserve Shaka fallback evidence

This commit is contained in:
4gray committed 2026-07-31 20:32:49 +02:00
1 parent a9b601bee6
commit 50ca78d3a8
9 files changed
+100 -8

No files matched your search

@@ -3,4 +3,4 @@ type: fix
area: playback
---
Shaka playback errors now use exact engine evidence, keep recoverable retries from becoming terminal diagnostics, and omit provider URLs, credentials, and response data from technical details.
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.
+3 -1
View File
@@ -213,7 +213,9 @@ Key files:
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`. See the CLAUDE.md "Video Players" feature entry and
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
+3 -1
View File
@@ -737,7 +737,9 @@ 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`; details in
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
`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`.
+11 -1
View File
@@ -409,7 +409,17 @@ not prove terminal failure and is ignored. Exact public code/category pairs may
classify network, DRM/encryption, manifest/parsing, or media/decode failures.
Ambiguous evidence stays `unknown-playback-error`: in particular, the Manifest
category alone is not container incompatibility, and Shaka messages never infer
CORS, codec, DRM, container, or stage.
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.
A failed public `Player.isBrowserSupported()` preflight is not a Shaka error
and therefore retains fully unknown technical evidence instead of being
mislabelled as an unsupported container. For clear DASH, the diagnostic still
offers configured MPV/VLC actions because the failure is specific to the web
engine. KODIPROP DRM sources keep external fallback disabled because external
players do not receive their key configuration.
Shaka messages, URLs, headers, request/response bodies, credentials,
license/key payloads, and arbitrary `error.data` objects are neither retained
+5
View File
@@ -1003,6 +1003,11 @@ player in settings.
only the documented direct/nested `BAD_HTTP_STATUS` slot may contribute a
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.
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:
@@ -91,6 +91,7 @@ export const SHAKA_ERROR_CODE = {
WEBTRANSPORT_INITIALIZATION_FAILED: 4057,
DASH_INVALID_JSON: 4061,
DASH_UNSUPPORTED_ESSENTIAL_PROPERTY: 4063,
STREAMING_ENGINE_STARTUP_INVALID_STATE: 5006,
NO_RECOGNIZED_KEY_SYSTEMS: 6000,
REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE: 6001,
FAILED_TO_CREATE_CDM: 6002,
@@ -197,6 +197,19 @@ describe('Shaka playback evidence', () => {
expect(evidence.httpStatus).toBeUndefined();
});
it('preserves the exact public streaming startup code as unknown failure evidence', () => {
expect(
createEvidence({ severity: 2, category: 5, code: 5006 }, 'terminal')
).toEqual({
severity: 'critical',
category: 'streaming',
engineCode: 5006,
disposition: 'terminal',
stage: 'unknown',
failure: 'unknown',
});
});
it.each([
{
label: 'manifest parsing',
@@ -258,6 +258,34 @@ describe('ShakaVideoSession', () => {
expect(session.getPlayer()).toBeNull();
});
it('preserves the exact streaming startup error through the session boundary', async () => {
const environment = createFakeShakaEnvironment({
onCreate: (player) => {
player.loadResult = Promise.reject({
severity: 2,
category: 5,
code: 5006,
});
},
});
const session = createSession(environment);
session.start(video, 'http://example.com/startup-failed.mpd');
await flush();
expect(issues).toHaveLength(1);
expect(issues[0].code).toBe(
PlaybackDiagnosticCode.UnknownPlaybackError
);
expect(issues[0].shaka).toEqual({
severity: 'critical',
category: 'streaming',
engineCode: 5006,
disposition: 'terminal',
stage: 'unknown',
failure: 'unknown',
});
});
it('emits a critical in-flight error once before teardown interrupts load', async () => {
const environment = createFakeShakaEnvironment({
onCreate: (player) => {
@@ -436,7 +464,7 @@ describe('ShakaVideoSession', () => {
expect(player.selectTextTrackCalls).toHaveLength(2);
});
it('emits an unsupported-container diagnostic when the browser lacks MSE/EME', async () => {
it('keeps external fallback available when the browser cannot run Shaka for clear DASH', async () => {
const environment = createFakeShakaEnvironment();
environment.browserSupported = false;
const session = createSession(environment);
@@ -456,6 +484,26 @@ describe('ShakaVideoSession', () => {
stage: 'unknown',
failure: 'unknown',
});
expect(issues[0].externalFallbackRecommended).toBe(true);
});
it('keeps external fallback unavailable when browser support fails for KODIPROP DRM', async () => {
const environment = createFakeShakaEnvironment();
environment.browserSupported = false;
const session = createSession(environment);
session.start(video, 'http://example.com/encrypted.mpd', {
licenseType: 'clearkey',
supported: true,
clearKeys: { abc: 'def' },
});
await flush();
expect(environment.instances).toHaveLength(0);
expect(issues).toHaveLength(1);
expect(issues[0].code).toBe(
PlaybackDiagnosticCode.UnknownPlaybackError
);
expect(issues[0].externalFallbackRecommended).toBe(false);
});
it('destroy tears down the engine and blocks later starts', async () => {
@@ -143,7 +143,12 @@ export class ShakaVideoSession {
}
if (!module.Player.isBrowserSupported()) {
this.emitTerminalIfCurrent(generation, null, url);
this.emitTerminalIfCurrent(
generation,
null,
url,
drm === undefined
);
return;
}
@@ -365,7 +370,8 @@ export class ShakaVideoSession {
private emitTerminalIfCurrent(
generation: number,
error: Partial<ShakaErrorLike> | null,
url: string
url: string,
externalFallbackRecommended?: boolean
): void {
const issue = classifyShakaPlaybackIssue(
error,
@@ -373,7 +379,12 @@ export class ShakaVideoSession {
ShakaDisposition.Terminal
);
if (issue) {
this.emitIfCurrent(generation, issue);
this.emitIfCurrent(
generation,
externalFallbackRecommended === undefined
? issue
: { ...issue, externalFallbackRecommended }
);
}
}