From f547219dd52590f27f4a3469afa1f6bd38f78279 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 24 Jul 2026 08:04:43 +0200 Subject: [PATCH] fix(playback): suppress unusable external fallback for ClearKey DRM failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runtime DRM errors on channels that carry KODIPROP ClearKey config (wrong or rotated keys) advertised MPV/VLC fallback actions, but external players never receive the license config — the fallback could only fail differently. DRM-classified diagnostics from such channels no longer recommend external players; clear channels keep the hint. Co-Authored-By: Claude Fable 5 --- .../shaka-engine/shaka-video-session.spec.ts | 25 ++++++++++ .../lib/shaka-engine/shaka-video-session.ts | 48 +++++++++++++++---- 2 files changed, 65 insertions(+), 8 deletions(-) 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 56b4a7f0b..c30988adf 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 @@ -107,11 +107,36 @@ describe('ShakaVideoSession', () => { player.dispatch('error', { severity: 2, category: 6, code: 6001 }); expect(issues).toHaveLength(1); expect(issues[0].code).toBe(PlaybackDiagnosticCode.DrmOrEncryption); + // Without KODIPROP config the DRM hint may still help externally. + expect(issues[0].externalFallbackRecommended).toBe(true); // Critical errors end playback: the dead engine must be torn down. expect(player.destroyCount).toBe(1); expect(session.getPlayer()).toBeNull(); }); + it('drops the external-fallback hint for DRM errors on ClearKey channels', async () => { + const environment = createFakeShakaEnvironment(); + const session = createSession(environment); + session.start(video, 'http://example.com/enc.mpd', { + licenseType: 'clearkey', + supported: true, + clearKeys: { abc: 'def' }, + }); + await flush(); + + environment.instances[0].dispatch('error', { + severity: 2, + category: 6, + code: 6001, + }); + + expect(issues).toHaveLength(1); + expect(issues[0].code).toBe(PlaybackDiagnosticCode.DrmOrEncryption); + // MPV/VLC never receive the KODIPROP license config, so they are not + // offered as a fallback for key failures on ClearKey channels. + expect(issues[0].externalFallbackRecommended).toBe(false); + }); + it('emits a diagnostic and tears the engine down when load rejects', async () => { const loadError = { severity: 2, category: 4, code: 4001 }; const environment = createFakeShakaEnvironment({ diff --git a/libs/ui/playback/src/lib/shaka-engine/shaka-video-session.ts b/libs/ui/playback/src/lib/shaka-engine/shaka-video-session.ts index acc6cfd2e..8fc365f22 100644 --- a/libs/ui/playback/src/lib/shaka-engine/shaka-video-session.ts +++ b/libs/ui/playback/src/lib/shaka-engine/shaka-video-session.ts @@ -165,7 +165,7 @@ export class ShakaVideoSession { const player = new module.Player(); this.player = player; - this.bindPlayerListeners(player, generation, url); + this.bindPlayerListeners(player, generation, url, drm !== undefined); if (drm?.clearKeys) { player.configure({ drm: { clearKeys: drm.clearKeys } }); @@ -175,7 +175,13 @@ export class ShakaVideoSession { await player.attach(video); await player.load(url); } catch (error: unknown) { - this.handleLoadFailure(generation, player, url, error); + this.handleLoadFailure( + generation, + player, + url, + error, + drm !== undefined + ); return; } @@ -195,7 +201,8 @@ export class ShakaVideoSession { generation: number, player: ShakaPlayerLike, url: string, - error: unknown + error: unknown, + drmProvided: boolean ): void { const shakaError = asShakaError(error); if ( @@ -207,9 +214,12 @@ export class ShakaVideoSession { } this.config.emitPlaybackIssue( - classifyShakaPlaybackIssue( - shakaError ?? { message: toErrorMessage(error) }, - this.createMetadata(url) + this.withoutUnusableDrmFallback( + classifyShakaPlaybackIssue( + shakaError ?? { message: toErrorMessage(error) }, + this.createMetadata(url) + ), + drmProvided ) ); // Never leave a non-functional engine attached to the media element @@ -217,10 +227,26 @@ export class ShakaVideoSession { this.beginPlayerTeardown(); } + /** + * For channels carrying KODIPROP ClearKey config, DRM failures (wrong or + * rotated keys, …) cannot be solved by MPV/VLC either — they never + * receive the license config — so the diagnostic must not offer them. + */ + private withoutUnusableDrmFallback( + issue: PlaybackDiagnostic, + drmProvided: boolean + ): PlaybackDiagnostic { + if (!drmProvided || issue.code !== DiagnosticCode.DrmOrEncryption) { + return issue; + } + return { ...issue, externalFallbackRecommended: false }; + } + private bindPlayerListeners( player: ShakaPlayerLike, generation: number, - url: string + url: string, + drmProvided: boolean ): void { const errorListener = (event: Event): void => { if (this.isStale(generation) || this.player !== player) { @@ -234,7 +260,13 @@ export class ShakaVideoSession { } this.config.emitPlaybackIssue( - classifyShakaPlaybackIssue(detail, this.createMetadata(url)) + this.withoutUnusableDrmFallback( + classifyShakaPlaybackIssue( + detail, + this.createMetadata(url) + ), + drmProvided + ) ); // Critical errors end playback; never leave the dead engine // attached or exposed to the shared-controls bridge.