fix(playback): suppress unusable external fallback for ClearKey DRM failures

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 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-24 08:04:43 +02:00
1 parent 13cc563c5b
commit f547219dd5
2 files changed
+65 -8

No files matched your search

@@ -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({
@@ -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.