fix(playback): reveal capped external fallback

This commit is contained in:
4gray committed 2026-08-09 12:52:05 +02:00
1 parent 3bbb35ba30
commit 9e6c67ff48
2 files changed
+116 -2

No files matched your search

@@ -94,6 +94,52 @@ describe('createWebPlayerRecommendations external outcomes', () => {
expect(targets(result)).not.toContain(InlinePlaybackPlayer.Html5);
});
it('keeps a failed external target and reveals its capped sibling', () => {
const result = recommendations({
diagnostic: {
...DIAGNOSTIC,
code: PlaybackDiagnosticCode.DrmOrEncryption,
source: PlaybackDiagnosticSource.Hls,
container: 'm3u8',
mimeType: 'application/vnd.apple.mpegurl',
},
attemptedTargets: new Set(['mpv']),
externalStates: externalStates({
mpv: { attempts: 1, sessionId: null, status: 'error' },
}),
});
expect(targets(result)).toEqual([
InlinePlaybackPlayer.Html5,
'vlc',
'mpv',
]);
expect(result).toHaveLength(3);
});
it('retains a previously attempted target that the initial cap omitted', () => {
const result = recommendations({
diagnostic: {
...DIAGNOSTIC,
code: PlaybackDiagnosticCode.DrmOrEncryption,
source: PlaybackDiagnosticSource.Hls,
container: 'm3u8',
mimeType: 'application/vnd.apple.mpegurl',
},
attemptedTargets: new Set(['vlc']),
externalStates: externalStates({
vlc: { attempts: 1, sessionId: null, status: 'error' },
}),
});
expect(targets(result)).toEqual([
InlinePlaybackPlayer.Html5,
'mpv',
'vlc',
]);
expect(result).toHaveLength(3);
});
it('preserves policy order for equal attempt counts and stays capped at three', () => {
const result = recommendations({
attemptedTargets: new Set(['mpv', 'vlc']),
@@ -5,6 +5,7 @@ import {
resolvePlaybackSourceKind,
type PlaybackDiagnostic,
type PlaybackRecommendation,
type PlaybackRecommendationContext,
type PlaybackRecommendationTarget,
} from '@iptvnator/playback/util';
import {
@@ -34,7 +35,7 @@ export function createWebPlayerRecommendations(options: {
(target) => target !== 'mpv' && target !== 'vlc'
)
);
const recommendations = recommendPlaybackRecovery({
const context: PlaybackRecommendationContext = {
diagnostic: options.diagnostic,
activeTarget: options.binding.target,
attemptedTargets: attemptedInlineTargets,
@@ -52,13 +53,69 @@ export function createWebPlayerRecommendations(options: {
externalTransferable: options.playbackExternallyTransferable,
},
alternativeSourceCount: options.alternativeSourceCount,
});
};
const recommendations = revealCappedExternalSiblings(
recommendPlaybackRecovery(context),
context,
options.externalStates
);
return rerankExternalRecommendations(
recommendations,
options.externalStates
);
}
function revealCappedExternalSiblings(
recommendations: readonly PlaybackRecommendation[],
context: PlaybackRecommendationContext,
states: ExternalRecoveryStates
): readonly PlaybackRecommendation[] {
const externalTargets: readonly ExternalPlayerName[] = ['mpv', 'vlc'];
if (!externalTargets.some((target) => states[target].attempts > 0)) {
return recommendations;
}
const retained = [...recommendations];
for (const target of externalTargets) {
if (
retained.some(
(recommendation) =>
isExternalRecommendation(recommendation) &&
recommendation.target === target
)
) {
continue;
}
const siblingTarget: ExternalPlayerName =
target === 'mpv' ? 'vlc' : 'mpv';
const candidate = recommendPlaybackRecovery({
...context,
attemptedTargets: new Set([
...context.attemptedTargets,
siblingTarget,
]),
}).find(
(recommendation) =>
isExternalRecommendation(recommendation) &&
recommendation.target === target
);
if (!candidate) {
continue;
}
if (retained.length >= 3) {
const replaceIndex = retained.findLastIndex(
(recommendation, index) =>
index > 0 && !isExternalRecommendation(recommendation)
);
if (replaceIndex < 0) {
continue;
}
retained.splice(replaceIndex, 1);
}
retained.push(candidate);
}
return retained;
}
function rerankExternalRecommendations(
recommendations: readonly PlaybackRecommendation[],
states: ExternalRecoveryStates
@@ -99,6 +156,17 @@ function rerankExternalRecommendations(
});
}
function isExternalRecommendation(
recommendation: PlaybackRecommendation
): recommendation is Extract<PlaybackRecommendation, { readonly action: 'player' }> & {
readonly target: ExternalPlayerName;
} {
return (
recommendation.action === 'player' &&
(recommendation.target === 'mpv' || recommendation.target === 'vlc')
);
}
export function isPlaybackExternallyTransferable(
playback: ResolvedPortalPlayback
): boolean {