mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -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
304 lines
9.3 KiB
TypeScript
304 lines
9.3 KiB
TypeScript
import {
|
|
InlinePlaybackPlayer,
|
|
PlaybackDiagnosticCode,
|
|
PlaybackDiagnosticSource,
|
|
type PlaybackDiagnostic,
|
|
} from './diagnostics/playback-diagnostics.model';
|
|
import {
|
|
PlaybackEngineFamily,
|
|
PlaybackSourceKind,
|
|
} from './playback-recommendation.model';
|
|
import {
|
|
createPlaybackTargetCapabilities,
|
|
getInlinePlaybackEngineFamily,
|
|
resolvePlaybackSourceKind,
|
|
} from './playback-target-capabilities';
|
|
|
|
function diagnostic(
|
|
overrides: Partial<PlaybackDiagnostic>
|
|
): PlaybackDiagnostic {
|
|
return {
|
|
code: PlaybackDiagnosticCode.UnknownPlaybackError,
|
|
source: PlaybackDiagnosticSource.Source,
|
|
sourceUrl: 'https://example.com/stream',
|
|
container: '',
|
|
audioCodecs: [],
|
|
videoCodecs: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('playback target capabilities', () => {
|
|
it.each([
|
|
[
|
|
PlaybackSourceKind.Hls,
|
|
InlinePlaybackPlayer.VideoJs,
|
|
PlaybackEngineFamily.Vhs,
|
|
],
|
|
[
|
|
PlaybackSourceKind.Hls,
|
|
InlinePlaybackPlayer.Html5,
|
|
PlaybackEngineFamily.HlsJs,
|
|
],
|
|
[
|
|
PlaybackSourceKind.Hls,
|
|
InlinePlaybackPlayer.ArtPlayer,
|
|
PlaybackEngineFamily.HlsJs,
|
|
],
|
|
[
|
|
PlaybackSourceKind.MpegTs,
|
|
InlinePlaybackPlayer.VideoJs,
|
|
PlaybackEngineFamily.MpegTsJs,
|
|
],
|
|
[
|
|
PlaybackSourceKind.MpegTs,
|
|
InlinePlaybackPlayer.Html5,
|
|
PlaybackEngineFamily.MpegTsJs,
|
|
],
|
|
[
|
|
PlaybackSourceKind.MpegTs,
|
|
InlinePlaybackPlayer.ArtPlayer,
|
|
PlaybackEngineFamily.MpegTsJs,
|
|
],
|
|
[PlaybackSourceKind.Dash, InlinePlaybackPlayer.VideoJs, null],
|
|
[
|
|
PlaybackSourceKind.Dash,
|
|
InlinePlaybackPlayer.Html5,
|
|
PlaybackEngineFamily.Shaka,
|
|
],
|
|
[
|
|
PlaybackSourceKind.Dash,
|
|
InlinePlaybackPlayer.ArtPlayer,
|
|
PlaybackEngineFamily.Shaka,
|
|
],
|
|
[
|
|
PlaybackSourceKind.Native,
|
|
InlinePlaybackPlayer.VideoJs,
|
|
PlaybackEngineFamily.NativeMedia,
|
|
],
|
|
[
|
|
PlaybackSourceKind.Native,
|
|
InlinePlaybackPlayer.Html5,
|
|
PlaybackEngineFamily.NativeMedia,
|
|
],
|
|
[
|
|
PlaybackSourceKind.Native,
|
|
InlinePlaybackPlayer.ArtPlayer,
|
|
PlaybackEngineFamily.NativeMedia,
|
|
],
|
|
] as const)('maps %s on %s to %s', (sourceKind, target, expectedFamily) => {
|
|
expect(getInlinePlaybackEngineFamily(sourceKind, target)).toBe(
|
|
expectedFamily
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
[PlaybackDiagnosticSource.Hls, {}, PlaybackSourceKind.Hls],
|
|
[
|
|
PlaybackDiagnosticSource.Vhs,
|
|
{ container: 'm3u8' },
|
|
PlaybackSourceKind.Hls,
|
|
],
|
|
[
|
|
PlaybackDiagnosticSource.Source,
|
|
{ container: 'm3u' },
|
|
PlaybackSourceKind.Hls,
|
|
],
|
|
[
|
|
PlaybackDiagnosticSource.Vhs,
|
|
{
|
|
mimeType: ' Application/Vnd.Apple.MPEGURL; charset=UTF-8 ',
|
|
},
|
|
PlaybackSourceKind.Hls,
|
|
],
|
|
[
|
|
PlaybackDiagnosticSource.Source,
|
|
{ mimeType: 'application/x-mpegurl' },
|
|
PlaybackSourceKind.Hls,
|
|
],
|
|
[
|
|
PlaybackDiagnosticSource.Vhs,
|
|
{ mimeType: 'audio/x-mpegurl' },
|
|
PlaybackSourceKind.Hls,
|
|
],
|
|
[PlaybackDiagnosticSource.Shaka, {}, PlaybackSourceKind.Dash],
|
|
[
|
|
PlaybackDiagnosticSource.Source,
|
|
{ container: 'mpd' },
|
|
PlaybackSourceKind.Dash,
|
|
],
|
|
[
|
|
PlaybackDiagnosticSource.Vhs,
|
|
{ container: 'mpd' },
|
|
PlaybackSourceKind.Dash,
|
|
],
|
|
[
|
|
PlaybackDiagnosticSource.Source,
|
|
{ mimeType: ' Application/Dash+XML; profile=live ' },
|
|
PlaybackSourceKind.Dash,
|
|
],
|
|
[PlaybackDiagnosticSource.MpegTs, {}, PlaybackSourceKind.MpegTs],
|
|
[PlaybackDiagnosticSource.Native, {}, PlaybackSourceKind.Native],
|
|
] as const)(
|
|
'resolves %s diagnostics with %o as %s',
|
|
(source, metadata, expectedKind) => {
|
|
expect(
|
|
resolvePlaybackSourceKind(diagnostic({ source, ...metadata }))
|
|
).toBe(expectedKind);
|
|
}
|
|
);
|
|
|
|
it.each([
|
|
diagnostic({ source: PlaybackDiagnosticSource.Source }),
|
|
diagnostic({
|
|
source: PlaybackDiagnosticSource.Source,
|
|
container: 'mp4',
|
|
mimeType: 'video/mp4',
|
|
}),
|
|
diagnostic({
|
|
source: PlaybackDiagnosticSource.Vhs,
|
|
container: 'mpd',
|
|
mimeType: 'application/vnd.apple.mpegurl',
|
|
}),
|
|
diagnostic({
|
|
source: PlaybackDiagnosticSource.Source,
|
|
container: 'm3u8',
|
|
mimeType: 'application/dash+xml',
|
|
}),
|
|
diagnostic({
|
|
source: PlaybackDiagnosticSource.Vhs,
|
|
mimeType: 'application/not-mpegurl',
|
|
}),
|
|
])('keeps insufficient or contradictory evidence unknown', (issue) => {
|
|
expect(resolvePlaybackSourceKind(issue)).toBe(
|
|
PlaybackSourceKind.Unknown
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
PlaybackDiagnosticSource.Hls,
|
|
{ container: 'mpd', mimeType: 'application/dash+xml' },
|
|
PlaybackSourceKind.Hls,
|
|
],
|
|
[
|
|
PlaybackDiagnosticSource.MpegTs,
|
|
{ container: 'mpd', mimeType: 'application/dash+xml' },
|
|
PlaybackSourceKind.MpegTs,
|
|
],
|
|
[
|
|
PlaybackDiagnosticSource.Shaka,
|
|
{
|
|
container: 'm3u8',
|
|
mimeType: 'application/vnd.apple.mpegurl',
|
|
},
|
|
PlaybackSourceKind.Dash,
|
|
],
|
|
[
|
|
PlaybackDiagnosticSource.Native,
|
|
{
|
|
container: 'm3u8',
|
|
mimeType: 'application/vnd.apple.mpegurl',
|
|
},
|
|
PlaybackSourceKind.Native,
|
|
],
|
|
] as const)(
|
|
'treats engine-specific %s source evidence as authoritative',
|
|
(source, metadata, expectedKind) => {
|
|
expect(
|
|
resolvePlaybackSourceKind(diagnostic({ source, ...metadata }))
|
|
).toBe(expectedKind);
|
|
}
|
|
);
|
|
|
|
it('creates capabilities in canonical order', () => {
|
|
expect(
|
|
createPlaybackTargetCapabilities({
|
|
sourceKind: PlaybackSourceKind.Hls,
|
|
managedExternalPlayersAvailable: true,
|
|
})
|
|
).toEqual([
|
|
{
|
|
kind: 'inline',
|
|
target: InlinePlaybackPlayer.VideoJs,
|
|
available: true,
|
|
engineFamily: PlaybackEngineFamily.Vhs,
|
|
},
|
|
{
|
|
kind: 'inline',
|
|
target: InlinePlaybackPlayer.Html5,
|
|
available: true,
|
|
engineFamily: PlaybackEngineFamily.HlsJs,
|
|
},
|
|
{
|
|
kind: 'inline',
|
|
target: InlinePlaybackPlayer.ArtPlayer,
|
|
available: true,
|
|
engineFamily: PlaybackEngineFamily.HlsJs,
|
|
},
|
|
{ kind: 'external', target: 'mpv', available: true },
|
|
{ kind: 'external', target: 'vlc', available: true },
|
|
]);
|
|
});
|
|
|
|
it('marks Video.js unavailable for DASH and mirrors external availability', () => {
|
|
expect(
|
|
createPlaybackTargetCapabilities({
|
|
sourceKind: PlaybackSourceKind.Dash,
|
|
managedExternalPlayersAvailable: false,
|
|
})
|
|
).toEqual([
|
|
{
|
|
kind: 'inline',
|
|
target: InlinePlaybackPlayer.VideoJs,
|
|
available: false,
|
|
engineFamily: null,
|
|
},
|
|
{
|
|
kind: 'inline',
|
|
target: InlinePlaybackPlayer.Html5,
|
|
available: true,
|
|
engineFamily: PlaybackEngineFamily.Shaka,
|
|
},
|
|
{
|
|
kind: 'inline',
|
|
target: InlinePlaybackPlayer.ArtPlayer,
|
|
available: true,
|
|
engineFamily: PlaybackEngineFamily.Shaka,
|
|
},
|
|
{ kind: 'external', target: 'mpv', available: false },
|
|
{ kind: 'external', target: 'vlc', available: false },
|
|
]);
|
|
});
|
|
|
|
it('fails closed for unknown inline source capabilities', () => {
|
|
expect(
|
|
createPlaybackTargetCapabilities({
|
|
sourceKind: PlaybackSourceKind.Unknown,
|
|
managedExternalPlayersAvailable: true,
|
|
})
|
|
).toEqual([
|
|
{
|
|
kind: 'inline',
|
|
target: InlinePlaybackPlayer.VideoJs,
|
|
available: false,
|
|
engineFamily: null,
|
|
},
|
|
{
|
|
kind: 'inline',
|
|
target: InlinePlaybackPlayer.Html5,
|
|
available: false,
|
|
engineFamily: null,
|
|
},
|
|
{
|
|
kind: 'inline',
|
|
target: InlinePlaybackPlayer.ArtPlayer,
|
|
available: false,
|
|
engineFamily: null,
|
|
},
|
|
{ kind: 'external', target: 'mpv', available: true },
|
|
{ kind: 'external', target: 'vlc', available: true },
|
|
]);
|
|
});
|
|
});
|