fix(playback): reset the media element for rejected DRM and widen ClearKey fallback suppression

- Switching from a playing stream to an unsupported-DRM DASH channel
  loads no new source, but play() still ran and the un-loaded element
  could resume the previous stream underneath the diagnostic banner.
  The HTML5 player now resets the element instead of playing.
- Any inline failure on a KODIPROP ClearKey channel (manifest, codec,
  media, network — not just DRM-category errors) is unsolvable in
  MPV/VLC, which never receive the license config; the external
  fallback hint is now suppressed for all diagnostics of such channels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-24 08:15:25 +02:00
1 parent ccc1752a3a
commit 208d8d0fc2
4 files changed
+61 -6

No files matched your search

@@ -224,6 +224,35 @@ describe('HtmlVideoPlayerComponent', () => {
);
});
it('does not resume the previous source when a channel declares unsupported DRM', () => {
const video = component.videoPlayer.nativeElement;
const loadSpy = jest
.spyOn(video, 'load')
.mockImplementation(() => undefined);
const playSpy = jest.spyOn(video, 'play').mockResolvedValue(undefined);
component.playChannel({
...TEST_CHANNEL,
url: 'https://stream.example/movie.mp4',
});
playSpy.mockClear();
loadSpy.mockClear();
component.playChannel({
...TEST_CHANNEL,
url: 'https://stream.example/enc.mpd',
drm: {
licenseType: 'com.widevine.alpha',
supported: false,
},
});
// No source is loaded for unsupported DRM: play() must not run, and
// the element is reset so the old stream cannot keep playing.
expect(playSpy).not.toHaveBeenCalled();
expect(loadSpy).toHaveBeenCalledTimes(1);
});
it('emits a playback issue when the native video element reports an unsupported source', () => {
const issues: unknown[] = [];
component.channel = TEST_CHANNEL;
@@ -197,7 +197,14 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
url,
channel.drm
);
this.handlePlayOperation();
if (channel.drm && !channel.drm.supported) {
// No source is loaded for unsupported DRM; reset the
// element so the previous stream cannot resume playing
// underneath the diagnostic banner.
this.videoPlayer.nativeElement.load();
} else {
this.handlePlayOperation();
}
} else if (
(extension === 'ts' || !extension) &&
mpegts.isSupported()
@@ -114,7 +114,7 @@ describe('ShakaVideoSession', () => {
expect(session.getPlayer()).toBeNull();
});
it('drops the external-fallback hint for DRM errors on ClearKey channels', async () => {
it('drops the external-fallback hint for every failure on ClearKey channels', async () => {
const environment = createFakeShakaEnvironment();
const session = createSession(environment);
session.start(video, 'http://example.com/enc.mpd', {
@@ -135,6 +135,24 @@ describe('ShakaVideoSession', () => {
// 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);
// Non-DRM failures (media/codec/manifest/…) are equally unsolvable
// externally while the stream itself stays encrypted.
session.start(video, 'http://example.com/enc.mpd', {
licenseType: 'clearkey',
supported: true,
clearKeys: { abc: 'def' },
});
await flush();
environment.instances[1].dispatch('error', {
severity: 2,
category: 3,
code: 3016,
});
expect(issues).toHaveLength(2);
expect(issues[1].code).toBe(PlaybackDiagnosticCode.MediaDecodeError);
expect(issues[1].externalFallbackRecommended).toBe(false);
});
it('emits a diagnostic and tears the engine down when load rejects', async () => {
@@ -228,15 +228,16 @@ export class ShakaVideoSession {
}
/**
* 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.
* Channels carrying KODIPROP ClearKey config cannot be handed to MPV/VLC
* at all — external players never receive the license config, so the
* encrypted stream fails there regardless of what broke inline (DRM,
* manifest, codec, network, …). Suppress the fallback hint entirely.
*/
private withoutUnusableDrmFallback(
issue: PlaybackDiagnostic,
drmProvided: boolean
): PlaybackDiagnostic {
if (!drmProvided || issue.code !== DiagnosticCode.DrmOrEncryption) {
if (!drmProvided || !issue.externalFallbackRecommended) {
return issue;
}
return { ...issue, externalFallbackRecommended: false };