mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13: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
168 lines
5.2 KiB
TypeScript
168 lines
5.2 KiB
TypeScript
import {
|
|
type MpegTsPlaybackEngineDetails as MpegTsPlaybackEngineDetailsValue,
|
|
type MpegTsPlaybackEngineType as MpegTsPlaybackEngineTypeValue,
|
|
type MpegTsPlaybackEvidence,
|
|
type MpegTsPlaybackFailure as MpegTsPlaybackFailureValue,
|
|
type MpegTsPlaybackStage as MpegTsPlaybackStageValue,
|
|
MpegTsPlaybackDisposition,
|
|
MpegTsPlaybackEngineDetails,
|
|
MpegTsPlaybackEngineType,
|
|
MpegTsPlaybackFailure,
|
|
MpegTsPlaybackStage,
|
|
} from './mpegts-playback-evidence.model';
|
|
|
|
export const MPEGTS_DIAGNOSTIC_VERSION = '1.8.0';
|
|
|
|
interface MpegTsPlaybackCause {
|
|
readonly stage: MpegTsPlaybackStageValue;
|
|
readonly failure: MpegTsPlaybackFailureValue;
|
|
}
|
|
|
|
export function createMpegTsPlaybackEvidence(
|
|
type: unknown,
|
|
details: unknown,
|
|
info: unknown
|
|
): MpegTsPlaybackEvidence {
|
|
const engineType = normalizeEngineType(type);
|
|
const engineDetails = normalizeEngineDetails(details);
|
|
const cause = deriveMpegTsPlaybackCause(engineType, engineDetails);
|
|
const httpStatus =
|
|
engineType === MpegTsPlaybackEngineType.Network &&
|
|
engineDetails ===
|
|
MpegTsPlaybackEngineDetails.HttpStatusCodeInvalid
|
|
? readHttpStatus(info)
|
|
: undefined;
|
|
|
|
return {
|
|
engineType,
|
|
engineDetails,
|
|
disposition: MpegTsPlaybackDisposition.Terminal,
|
|
...cause,
|
|
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
};
|
|
}
|
|
|
|
function normalizeEngineType(value: unknown): MpegTsPlaybackEngineTypeValue {
|
|
switch (value) {
|
|
case MpegTsPlaybackEngineType.Network:
|
|
case MpegTsPlaybackEngineType.Media:
|
|
case MpegTsPlaybackEngineType.Other:
|
|
return value;
|
|
default:
|
|
return MpegTsPlaybackEngineType.Unknown;
|
|
}
|
|
}
|
|
|
|
function normalizeEngineDetails(
|
|
value: unknown
|
|
): MpegTsPlaybackEngineDetailsValue {
|
|
switch (value) {
|
|
case MpegTsPlaybackEngineDetails.NetworkException:
|
|
case MpegTsPlaybackEngineDetails.HttpStatusCodeInvalid:
|
|
case MpegTsPlaybackEngineDetails.ConnectingTimeout:
|
|
case MpegTsPlaybackEngineDetails.UnrecoverableEarlyEof:
|
|
case MpegTsPlaybackEngineDetails.MediaMseError:
|
|
case MpegTsPlaybackEngineDetails.FormatError:
|
|
case MpegTsPlaybackEngineDetails.FormatUnsupported:
|
|
case MpegTsPlaybackEngineDetails.CodecUnsupported:
|
|
return value;
|
|
default:
|
|
return MpegTsPlaybackEngineDetails.Unknown;
|
|
}
|
|
}
|
|
|
|
function deriveMpegTsPlaybackCause(
|
|
type: MpegTsPlaybackEngineTypeValue,
|
|
details: MpegTsPlaybackEngineDetailsValue
|
|
): MpegTsPlaybackCause {
|
|
if (type === MpegTsPlaybackEngineType.Network) {
|
|
return deriveNetworkCause(details);
|
|
}
|
|
if (type === MpegTsPlaybackEngineType.Media) {
|
|
return deriveMediaCause(details);
|
|
}
|
|
return unknownCause();
|
|
}
|
|
|
|
function deriveNetworkCause(
|
|
details: MpegTsPlaybackEngineDetailsValue
|
|
): MpegTsPlaybackCause {
|
|
switch (details) {
|
|
case MpegTsPlaybackEngineDetails.HttpStatusCodeInvalid:
|
|
return cause(
|
|
MpegTsPlaybackStage.Loader,
|
|
MpegTsPlaybackFailure.Http
|
|
);
|
|
case MpegTsPlaybackEngineDetails.ConnectingTimeout:
|
|
return cause(
|
|
MpegTsPlaybackStage.Loader,
|
|
MpegTsPlaybackFailure.Timeout
|
|
);
|
|
case MpegTsPlaybackEngineDetails.NetworkException:
|
|
return cause(
|
|
MpegTsPlaybackStage.Loader,
|
|
MpegTsPlaybackFailure.Network
|
|
);
|
|
case MpegTsPlaybackEngineDetails.UnrecoverableEarlyEof:
|
|
return cause(
|
|
MpegTsPlaybackStage.Loader,
|
|
MpegTsPlaybackFailure.TruncatedStream
|
|
);
|
|
default:
|
|
return unknownCause();
|
|
}
|
|
}
|
|
|
|
function deriveMediaCause(
|
|
details: MpegTsPlaybackEngineDetailsValue
|
|
): MpegTsPlaybackCause {
|
|
switch (details) {
|
|
case MpegTsPlaybackEngineDetails.FormatError:
|
|
case MpegTsPlaybackEngineDetails.FormatUnsupported:
|
|
return cause(
|
|
MpegTsPlaybackStage.Demux,
|
|
MpegTsPlaybackFailure.Format
|
|
);
|
|
case MpegTsPlaybackEngineDetails.CodecUnsupported:
|
|
return cause(
|
|
MpegTsPlaybackStage.Demux,
|
|
MpegTsPlaybackFailure.Codec
|
|
);
|
|
case MpegTsPlaybackEngineDetails.MediaMseError:
|
|
return cause(
|
|
MpegTsPlaybackStage.MediaSource,
|
|
MpegTsPlaybackFailure.MediaSource
|
|
);
|
|
default:
|
|
return unknownCause();
|
|
}
|
|
}
|
|
|
|
function cause(
|
|
stage: MpegTsPlaybackStageValue,
|
|
failure: MpegTsPlaybackFailureValue
|
|
): MpegTsPlaybackCause {
|
|
return { stage, failure };
|
|
}
|
|
|
|
function unknownCause(): MpegTsPlaybackCause {
|
|
return cause(
|
|
MpegTsPlaybackStage.Unknown,
|
|
MpegTsPlaybackFailure.Unknown
|
|
);
|
|
}
|
|
|
|
function readHttpStatus(info: unknown): number | undefined {
|
|
if (typeof info !== 'object' || info === null || Array.isArray(info)) {
|
|
return undefined;
|
|
}
|
|
|
|
const code = (info as Readonly<Record<string, unknown>>)['code'];
|
|
return typeof code === 'number' &&
|
|
Number.isInteger(code) &&
|
|
code >= 400 &&
|
|
code <= 599
|
|
? code
|
|
: undefined;
|
|
}
|