From e30811589e6c752060d6859fa9ae073e892708c5 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 24 Jul 2026 08:04:43 +0200 Subject: [PATCH] fix(m3u): symmetric DASH inline gate and lazy DRM for pre-upgrade playlists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../video-player.component.spec.ts | 50 +++++++++++++++++++ .../video-player/video-player.component.ts | 23 +++++---- .../interfaces/src/lib/channel.interface.ts | 2 + 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts index 5c96080fa..b4879d433 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts @@ -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', diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts index 5de7d36c7..a887f4a39 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts @@ -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(() => diff --git a/libs/shared/interfaces/src/lib/channel.interface.ts b/libs/shared/interfaces/src/lib/channel.interface.ts index e45e39069..2eb403ad7 100644 --- a/libs/shared/interfaces/src/lib/channel.interface.ts +++ b/libs/shared/interfaces/src/lib/channel.interface.ts @@ -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; }