fix(m3u): honor DASH catch-up URLs and drop unusable DRM fallbacks

Two Codex round-2 findings:

- The inline-playback DASH gate only examined the channel URL, while the
  external-player guard checks the resolved catch-up URL — a replay that
  resolves to an .mpd manifest with MPV/VLC configured ended up with no
  player at all. The gate now uses the effective playback URL
  (activePlaybackUrl ?? channel.url).
- The unsupported-DRM diagnostic advertised MPV/VLC fallback actions,
  but external players cannot receive the KODIPROP license config either
  — the diagnostic no longer recommends them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-24 00:02:56 +02:00
1 parent 61651b659b
commit 13cc563c5b
5 files changed
+43 -9

No files matched your search

@@ -646,6 +646,22 @@ describe('VideoPlayerComponent', () => {
expect(dataServiceMock.sendIpcEvent).not.toHaveBeenCalled();
});
it('routes catch-up playback that resolves to a DASH URL inline as well', () => {
syncStoreState(sampleChannel);
activePlaybackUrl.set('http://localhost/archive/replay.mpd');
player.set(VideoPlayer.MPV);
fixture.detectChanges();
const playerView = fixture.debugElement.query(
By.directive(StubWebPlayerViewComponent)
);
expect(playerView).not.toBeNull();
const stub =
playerView.componentInstance as StubWebPlayerViewComponent;
expect(stub.playerOverride()).toBe(VideoPlayer.Html5Player);
});
it('keeps ArtPlayer for DASH channels and forwards the ClearKey DRM config', () => {
const drm = {
licenseType: 'clearkey',
@@ -24,7 +24,7 @@ import { ResizableDirective } from '@iptvnator/ui/components';
import {
applyChannelNameStrip,
getM3uArchiveDays,
isDashChannel,
isDashStreamUrl,
isM3uCatchupPlaybackSupported,
} from '@iptvnator/shared/m3u-utils';
import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
@@ -174,9 +174,16 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
readonly archivePlaybackAvailable = computed(() =>
isM3uCatchupPlaybackSupported(this.activeChannel())
);
/** DASH (.mpd) channels always play inline via the Shaka engine. */
/**
* 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.
*/
readonly activeChannelIsDash = computed(() =>
isDashChannel(this.activeChannel())
isDashStreamUrl(
this.activePlaybackUrl() ?? this.activeChannel()?.url
)
);
/**
* Player forced for DASH channels: ArtPlayer keeps ArtPlayer (it has a
@@ -1009,9 +1016,10 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
return false;
}
// DASH channels bypass the external-player setting (radio precedent):
// MPV/VLC cannot receive the KODIPROP ClearKey configuration.
if (isDashChannel(channel)) {
// DASH playback bypasses the external-player setting (radio
// precedent): MPV/VLC cannot receive the KODIPROP ClearKey
// configuration. Checked on the effective (possibly catch-up) URL.
if (this.activeChannelIsDash()) {
return true;
}
@@ -237,6 +237,9 @@ export function createPlaybackDiagnostic(options: {
readonly details?: string;
readonly nativeErrorCode?: number;
readonly nativeErrorMessage?: string;
/** Overrides the code-derived recommendation, e.g. when external players
* are known to be unable to handle the stream either. */
readonly externalFallbackRecommended?: boolean;
}): PlaybackDiagnostic {
const {
code,
@@ -259,7 +262,9 @@ export function createPlaybackDiagnostic(options: {
details: details || undefined,
nativeErrorCode,
nativeErrorMessage,
externalFallbackRecommended: isExternalFallbackRecommended(code),
externalFallbackRecommended:
options.externalFallbackRecommended ??
isExternalFallbackRecommended(code),
};
}
@@ -86,7 +86,7 @@ describe('classifyShakaPlaybackIssue', () => {
});
describe('createUnsupportedDrmDiagnostic', () => {
it('creates a DRM diagnostic carrying the license type', () => {
it('creates a DRM diagnostic without recommending unusable external fallbacks', () => {
const issue = createUnsupportedDrmDiagnostic(
'com.widevine.alpha',
metadata
@@ -94,6 +94,8 @@ describe('createUnsupportedDrmDiagnostic', () => {
expect(issue.code).toBe(PlaybackDiagnosticCode.DrmOrEncryption);
expect(issue.source).toBe(PlaybackDiagnosticSource.Shaka);
expect(issue.details).toContain('com.widevine.alpha');
expect(issue.externalFallbackRecommended).toBe(true);
// MPV/VLC cannot receive KODIPROP license config, so the diagnostic
// must not offer them as a fallback.
expect(issue.externalFallbackRecommended).toBe(false);
});
});
@@ -111,6 +111,9 @@ export function createUnsupportedDrmDiagnostic(
details: licenseType
? `Unsupported DRM license configuration: ${licenseType}`
: 'Unsupported DRM license configuration',
// External MPV/VLC cannot receive the KODIPROP license config either,
// so offering them as a fallback would just fail differently.
externalFallbackRecommended: false,
});
}