mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
* docs(playback): design recovery recommendations * docs(playback): plan recovery recommendations * refactor(playback): extract diagnostic utilities * feat(playback): define recovery recommendation contracts * feat(playback): rank recovery recommendations * feat(playback): track session recovery attempts * feat(playback): identify content recovery sessions * feat(ui): add ranked playback diagnostic panel * feat(playback): switch temporarily to recommended players * test(playback): cover temporary player recommendation * test(playback): verify recommendation capability guards * docs(playback): document recovery recommendations * fix(playback): keep recovery keys credential-free * fix(playback): remove derived tracking ownership * fix(playback): preserve distinct recovery fallbacks * fix(playback): reset resume for new sources * fix(playback): preserve desktop recovery guidance * docs(playback): clarify recovery policy exceptions * fix(playback): reject stale progress updates * fix(playback): keep protected recovery guidance neutral * test(playback): cover stale progress output * fix(playback): neutralize protected diagnostic copy * fix(playback): harden runtime guidance ownership * fix(playback): stabilize recovery application ownership * fix(ci): classify playback util coverage * fix(e2e): preserve playback fixture bytes
145 lines
4.2 KiB
TypeScript
145 lines
4.2 KiB
TypeScript
import { InlinePlaybackPlayer } from './diagnostics/playback-diagnostics.model';
|
|
import {
|
|
PlaybackEngineFamily,
|
|
type PlaybackRecommendationContext,
|
|
type PlaybackRecommendationTarget,
|
|
type PlaybackTargetCapability,
|
|
} from './playback-recommendation.model';
|
|
import { getInlinePlaybackEngineFamily } from './playback-target-capabilities';
|
|
|
|
interface RuntimeCapabilityRecord {
|
|
readonly target: unknown;
|
|
readonly kind: unknown;
|
|
readonly available: unknown;
|
|
readonly engineFamily?: unknown;
|
|
}
|
|
|
|
export type PlaybackCapabilityIndex = ReadonlyMap<
|
|
PlaybackRecommendationTarget,
|
|
PlaybackTargetCapability
|
|
>;
|
|
|
|
const CANONICAL_TARGETS: readonly PlaybackRecommendationTarget[] = [
|
|
InlinePlaybackPlayer.VideoJs,
|
|
InlinePlaybackPlayer.Html5,
|
|
InlinePlaybackPlayer.ArtPlayer,
|
|
'mpv',
|
|
'vlc',
|
|
];
|
|
|
|
export function createPlaybackCapabilityIndex(
|
|
context: PlaybackRecommendationContext
|
|
): PlaybackCapabilityIndex | null {
|
|
const index = new Map<
|
|
PlaybackRecommendationTarget,
|
|
PlaybackTargetCapability
|
|
>();
|
|
for (const capability of context.targetCapabilities) {
|
|
if (!isValidCapability(context, capability, index)) {
|
|
return null;
|
|
}
|
|
index.set(capability.target, capability);
|
|
}
|
|
if (index.size !== CANONICAL_TARGETS.length) {
|
|
return null;
|
|
}
|
|
const active = index.get(context.activeTarget);
|
|
return active?.kind === 'inline' &&
|
|
active.available &&
|
|
active.engineFamily !== null
|
|
? index
|
|
: null;
|
|
}
|
|
|
|
export function hasAvailableCapability(
|
|
capabilityIndex: PlaybackCapabilityIndex | null,
|
|
target: PlaybackRecommendationTarget
|
|
): boolean {
|
|
return capabilityIndex?.get(target)?.available === true;
|
|
}
|
|
|
|
export function getAttemptedInlineFamilies(
|
|
context: PlaybackRecommendationContext,
|
|
capabilityIndex: PlaybackCapabilityIndex | null
|
|
): ReadonlySet<PlaybackEngineFamily> {
|
|
const families = new Set<PlaybackEngineFamily>();
|
|
if (capabilityIndex === null) {
|
|
return families;
|
|
}
|
|
for (const target of context.attemptedTargets) {
|
|
const capability = capabilityIndex.get(target);
|
|
if (
|
|
capability?.kind === 'inline' &&
|
|
capability.available &&
|
|
capability.engineFamily !== null
|
|
) {
|
|
families.add(capability.engineFamily);
|
|
}
|
|
}
|
|
return families;
|
|
}
|
|
|
|
export function isInlineFamilyAttempted(
|
|
target: PlaybackRecommendationTarget,
|
|
capabilityIndex: PlaybackCapabilityIndex | null,
|
|
attemptedFamilies: ReadonlySet<PlaybackEngineFamily>
|
|
): boolean {
|
|
const capability = capabilityIndex?.get(target);
|
|
return (
|
|
capability?.kind === 'inline' &&
|
|
capability.engineFamily !== null &&
|
|
attemptedFamilies.has(capability.engineFamily)
|
|
);
|
|
}
|
|
|
|
function isValidCapability(
|
|
context: PlaybackRecommendationContext,
|
|
capability: unknown,
|
|
index: PlaybackCapabilityIndex
|
|
): capability is PlaybackTargetCapability {
|
|
if (
|
|
!hasRequiredCapabilityFields(capability) ||
|
|
!isKnownTarget(capability.target) ||
|
|
index.has(capability.target) ||
|
|
typeof capability.available !== 'boolean'
|
|
) {
|
|
return false;
|
|
}
|
|
if (!isInlineTarget(capability.target)) {
|
|
return capability.kind === 'external';
|
|
}
|
|
if (capability.kind !== 'inline' || !('engineFamily' in capability)) {
|
|
return false;
|
|
}
|
|
const expectedFamily = getInlinePlaybackEngineFamily(
|
|
context.source.kind,
|
|
capability.target
|
|
);
|
|
return (
|
|
capability.engineFamily === expectedFamily &&
|
|
!(expectedFamily === null && capability.available)
|
|
);
|
|
}
|
|
|
|
function hasRequiredCapabilityFields(
|
|
value: unknown
|
|
): value is RuntimeCapabilityRecord {
|
|
return (
|
|
typeof value === 'object' &&
|
|
value !== null &&
|
|
'target' in value &&
|
|
'kind' in value &&
|
|
'available' in value
|
|
);
|
|
}
|
|
|
|
function isKnownTarget(value: unknown): value is PlaybackRecommendationTarget {
|
|
return CANONICAL_TARGETS.includes(value as PlaybackRecommendationTarget);
|
|
}
|
|
|
|
function isInlineTarget(
|
|
value: PlaybackRecommendationTarget
|
|
): value is InlinePlaybackPlayer {
|
|
return value !== 'mpv' && value !== 'vlc';
|
|
}
|