fix(m3u): symmetric DASH inline gate and lazy DRM for pre-upgrade playlists

- The inline DASH gate is now true when either the channel or the
  resolved catch-up URL is DASH, mirroring the external-player guard —
  a .mpd channel whose catch-up resolves to .m3u8 no longer ends up
  with no player at all.
- Playlists imported before the DRM feature carry no drm field, but the
  raw KODIPROP block survived in the stored items; the M3U page now
  falls back to extractDrmFromRaw(channel.raw) at playback time, so
  encrypted channels work without a re-import (Channel gains raw?).

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 f547219dd5
commit e30811589e
3 files changed
+66 -9

No files matched your search

@@ -662,6 +662,56 @@ describe('VideoPlayerComponent', () => {
expect(stub.playerOverride()).toBe(VideoPlayer.Html5Player);
});
it('keeps a DASH channel inline when its catch-up resolves to a non-DASH URL', () => {
syncStoreState({
...sampleChannel,
url: 'http://localhost/live.mpd',
} as Channel);
activePlaybackUrl.set('http://localhost/archive/replay.m3u8');
player.set(VideoPlayer.MPV);
fixture.detectChanges();
// The external-player guard declines DASH channels, so the inline
// player must stay — otherwise the session has no player at all.
expect(
fixture.debugElement.query(
By.directive(StubWebPlayerViewComponent)
)
).not.toBeNull();
});
it('extracts DRM lazily from raw KODIPROP for pre-upgrade playlists', () => {
const kid = '00112233445566778899aabbccddeeff';
const key = 'ffeeddccbbaa99887766554433221100';
syncStoreState({
...sampleChannel,
url: 'http://localhost/enc.mpd',
raw: [
'#EXTINF:-1,Encrypted',
'#KODIPROP:inputstream.adaptive.license_type=clearkey',
`#KODIPROP:inputstream.adaptive.license_key=${kid}:${key}`,
'http://localhost/enc.mpd',
].join('\r\n'),
} as Channel);
player.set(VideoPlayer.Html5Player);
fixture.detectChanges();
const stub = fixture.debugElement.query(
By.directive(StubWebPlayerViewComponent)
).componentInstance as StubWebPlayerViewComponent;
expect(stub.playback()).toEqual(
expect.objectContaining({
drm: {
licenseType: 'clearkey',
supported: true,
clearKeys: { [kid]: key },
},
})
);
});
it('keeps ArtPlayer for DASH channels and forwards the ClearKey DRM config', () => {
const drm = {
licenseType: 'clearkey',
@@ -24,6 +24,8 @@ import { ResizableDirective } from '@iptvnator/ui/components';
import {
applyChannelNameStrip,
getM3uArchiveDays,
extractDrmFromRaw,
isDashChannel,
isDashStreamUrl,
isM3uCatchupPlaybackSupported,
} from '@iptvnator/shared/m3u-utils';
@@ -175,15 +177,15 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
isM3uCatchupPlaybackSupported(this.activeChannel())
);
/**
* DASH (.mpd) playback always runs inline via the Shaka engine. Uses the
* effective playback URL, so catch-up/archive replays that resolve to a
* DASH manifest route inline too — matching the external-player guard in
* the m3u-state effects, which also checks the resolved URL.
* DASH (.mpd) playback always runs inline via the Shaka engine. True when
* either the channel itself or the resolved catch-up URL is DASH —
* mirroring the external-player guard in the m3u-state effects, so a
* DASH-flavored session can never end up with no player at all.
*/
readonly activeChannelIsDash = computed(() =>
isDashStreamUrl(
this.activePlaybackUrl() ?? this.activeChannel()?.url
)
readonly activeChannelIsDash = computed(
() =>
isDashStreamUrl(this.activePlaybackUrl() ?? undefined) ||
isDashChannel(this.activeChannel())
);
/**
* Player forced for DASH channels: ArtPlayer keeps ArtPlayer (it has a
@@ -303,7 +305,10 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
userAgent: http['user-agent'] || undefined,
referer: http.referrer || undefined,
origin: http.origin || undefined,
drm: playbackTarget.drm,
// Playlists imported before the DRM feature carry no drm field
// yet, but their raw KODIPROP block survived in the stored items
// — extract lazily so they work without a re-import.
drm: playbackTarget.drm ?? extractDrmFromRaw(playbackTarget.raw),
};
});
readonly sidebarStorageKey = computed(() =>
@@ -32,4 +32,6 @@ export interface Channel {
};
radio: string;
drm?: ChannelDrm;
/** Raw M3U block of the entry as preserved by the playlist parser. */
raw?: string;
}