Files
iptvnator/libs/playback/util/src/lib/diagnostics/playback-diagnostics.util.ts
T

329 lines
10 KiB
TypeScript

import type {
HlsPlaybackEvidence,
MpegTsPlaybackEvidence,
NativePlaybackErrorInput,
PlaybackDiagnostic,
PlaybackDiagnosticCode,
PlaybackDiagnosticSource,
PlaybackRuntimeSupport,
PlaybackSourceMetadata,
ShakaPlaybackEvidence,
VhsPlaybackEngineType as VhsPlaybackEngineTypeValue,
VhsPlaybackEvidence,
} from './playback-diagnostics.model';
import { PlaybackDiagnosticCode as DiagnosticCode } from './playback-diagnostics.model';
import { HlsPlaybackDisposition } from './playback-diagnostics.model';
import {
MpegTsPlaybackEngineDetails,
MpegTsPlaybackFailure,
} from './mpegts-playback-evidence.model';
import { PlaybackDiagnosticSource as DiagnosticSource } from './playback-diagnostics.model';
import {
VhsPlaybackEngineType,
VhsPlaybackMediaErrorCode,
} from './playback-diagnostics.model';
import { getHlsPlaybackDiagnosticCode } from './hls-playback-evidence.util';
import { isBrowserAccessFailure } from './playback-error-patterns.util';
import { isLikelyContainerIssue } from './playback-media-source.util';
import { createVhsPlaybackEvidence } from './vhs-playback-evidence.util';
export * from './playback-diagnostics.model';
export { createHlsPlaybackEvidence } from './hls-playback-evidence.util';
export { createMpegTsPlaybackEvidence } from './mpegts-playback-evidence.util';
export { createVhsPlaybackEvidence } from './vhs-playback-evidence.util';
export {
createPlaybackSourceMetadata,
getLikelyBrowserUnsupportedCodecLabels,
getPlaybackMediaExtensionFromUrl,
} from './playback-media-source.util';
const SOURCE_NOT_SUPPORTED_CODE = 4;
const DECODE_ERROR_CODE = 3;
const NETWORK_ERROR_CODE = 2;
const NATIVE_ERROR_TYPE_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
const VHS_NETWORK_ERROR_TYPES: ReadonlySet<VhsPlaybackEngineTypeValue> =
new Set([
VhsPlaybackEngineType.NetworkBadStatus,
VhsPlaybackEngineType.NetworkRequestFailed,
VhsPlaybackEngineType.NetworkRequestAborted,
VhsPlaybackEngineType.NetworkRequestTimeout,
VhsPlaybackEngineType.NetworkBodyParserFailed,
]);
export function classifyNativePlaybackIssue(
error: NativePlaybackErrorInput | null | undefined,
metadata: PlaybackSourceMetadata
): PlaybackDiagnostic {
const nativeErrorCode = error?.code;
const nativeErrorMessage = error?.message || undefined;
const httpStatus = getNativeHttpStatus(error?.status);
const nativeErrorType = getNativeErrorType(error?.metadata?.errorType);
const lowerNativeErrorMessage = nativeErrorMessage?.toLowerCase() ?? '';
if (httpStatus !== undefined) {
return createPlaybackDiagnostic({
code: DiagnosticCode.NetworkError,
source: DiagnosticSource.Native,
metadata,
nativeErrorCode,
nativeErrorMessage,
httpStatus,
nativeErrorType,
});
}
if (nativeErrorCode === NETWORK_ERROR_CODE) {
// Native MediaError details are often opaque for browser security
// failures. Only classify browser access when the runtime exposes a
// concrete CORS/mixed-content/CSP-style message.
return createPlaybackDiagnostic({
code: isBrowserAccessFailure(lowerNativeErrorMessage)
? DiagnosticCode.BrowserAccessError
: DiagnosticCode.NetworkError,
source: DiagnosticSource.Native,
metadata,
nativeErrorCode,
nativeErrorMessage,
nativeErrorType,
});
}
if (nativeErrorCode === DECODE_ERROR_CODE) {
return createPlaybackDiagnostic({
code: DiagnosticCode.MediaDecodeError,
source: DiagnosticSource.Native,
metadata,
nativeErrorCode,
nativeErrorMessage,
nativeErrorType,
});
}
if (nativeErrorCode === SOURCE_NOT_SUPPORTED_CODE) {
return createPlaybackDiagnostic({
code: isLikelyContainerIssue(metadata)
? DiagnosticCode.UnsupportedContainer
: DiagnosticCode.UnknownPlaybackError,
source: DiagnosticSource.Native,
metadata,
nativeErrorCode,
nativeErrorMessage,
nativeErrorType,
});
}
return createPlaybackDiagnostic({
code: DiagnosticCode.UnknownPlaybackError,
source: DiagnosticSource.Native,
metadata,
nativeErrorCode,
nativeErrorMessage,
nativeErrorType,
});
}
export function classifyVhsPlaybackIssue(
error: NativePlaybackErrorInput,
metadata: PlaybackSourceMetadata
): PlaybackDiagnostic {
const evidence = createVhsPlaybackEvidence(error);
const code = getVhsPlaybackDiagnosticCode(evidence, metadata);
return createPlaybackDiagnostic({
code,
source: DiagnosticSource.Vhs,
metadata,
nativeErrorCode:
typeof evidence.mediaErrorCode === 'number'
? evidence.mediaErrorCode
: undefined,
httpStatus: evidence.httpStatus,
vhs: evidence,
});
}
export function classifyHlsPlaybackIssue(
evidence: HlsPlaybackEvidence,
metadata: PlaybackSourceMetadata
): PlaybackDiagnostic | null {
if (evidence.disposition === HlsPlaybackDisposition.Recoverable) {
return null;
}
return createPlaybackDiagnostic({
code: getHlsPlaybackDiagnosticCode(evidence),
source: DiagnosticSource.Hls,
metadata,
httpStatus: evidence.httpStatus,
hls: evidence,
});
}
export function classifyMpegTsPlaybackIssue(
evidence: MpegTsPlaybackEvidence,
metadata: PlaybackSourceMetadata
): PlaybackDiagnostic {
return createPlaybackDiagnostic({
code: getMpegTsPlaybackDiagnosticCode(evidence),
source: DiagnosticSource.MpegTs,
metadata,
httpStatus: evidence.httpStatus,
mpegTs: evidence,
});
}
export type MediaTypeSupportProbe = (mimeType: string) => boolean | undefined;
export function classifyUnsupportedHlsManifestCodecs(
metadata: PlaybackSourceMetadata,
isTypeSupported: MediaTypeSupportProbe
): PlaybackDiagnostic | null {
if (!hasCodecs(metadata)) {
return null;
}
const codecList = [...metadata.videoCodecs, ...metadata.audioCodecs];
const mimeType = `video/mp4; codecs="${codecList.join(',')}"`;
if (isTypeSupported(mimeType) !== false) {
return null;
}
return createPlaybackDiagnostic({
code: DiagnosticCode.UnsupportedCodec,
source: DiagnosticSource.Source,
metadata,
details: mimeType,
});
}
export function createPlaybackDiagnostic(options: {
readonly code: PlaybackDiagnosticCode;
readonly source: PlaybackDiagnosticSource;
readonly metadata: PlaybackSourceMetadata;
readonly runtimeSupport?: PlaybackRuntimeSupport;
readonly details?: string;
readonly nativeErrorCode?: number;
readonly nativeErrorMessage?: string;
readonly httpStatus?: number;
readonly nativeErrorType?: string;
readonly vhs?: VhsPlaybackEvidence;
readonly hls?: HlsPlaybackEvidence;
readonly mpegTs?: MpegTsPlaybackEvidence;
readonly shaka?: ShakaPlaybackEvidence;
}): PlaybackDiagnostic {
const {
code,
source,
metadata,
runtimeSupport,
details,
nativeErrorCode,
nativeErrorMessage,
httpStatus,
nativeErrorType,
vhs,
hls,
mpegTs,
shaka,
} = options;
return {
code,
source,
...(metadata.drmSystems?.length
? { drmSystems: metadata.drmSystems }
: {}),
sourceUrl: metadata.url,
container: metadata.container,
mimeType: metadata.mimeType,
player: metadata.player,
audioCodecs: metadata.audioCodecs,
videoCodecs: metadata.videoCodecs,
runtimeSupport,
details: details || undefined,
nativeErrorCode,
nativeErrorMessage,
httpStatus,
nativeErrorType,
vhs,
hls,
mpegTs,
shaka,
};
}
function getMpegTsPlaybackDiagnosticCode(
evidence: MpegTsPlaybackEvidence
): PlaybackDiagnosticCode {
switch (evidence.failure) {
case MpegTsPlaybackFailure.Http:
case MpegTsPlaybackFailure.Timeout:
case MpegTsPlaybackFailure.Network:
return DiagnosticCode.NetworkError;
case MpegTsPlaybackFailure.TruncatedStream:
case MpegTsPlaybackFailure.MediaSource:
return DiagnosticCode.MediaDecodeError;
case MpegTsPlaybackFailure.Codec:
return DiagnosticCode.UnsupportedCodec;
case MpegTsPlaybackFailure.Format:
return evidence.engineDetails ===
MpegTsPlaybackEngineDetails.FormatUnsupported
? DiagnosticCode.UnsupportedContainer
: DiagnosticCode.MediaDecodeError;
default:
return DiagnosticCode.UnknownPlaybackError;
}
}
function getVhsPlaybackDiagnosticCode(
evidence: VhsPlaybackEvidence,
metadata: PlaybackSourceMetadata
): PlaybackDiagnosticCode {
if (
evidence.httpStatus !== undefined ||
VHS_NETWORK_ERROR_TYPES.has(evidence.engineType) ||
evidence.mediaErrorCode === VhsPlaybackMediaErrorCode.Network
) {
return DiagnosticCode.NetworkError;
}
if (
evidence.engineType ===
VhsPlaybackEngineType.StreamingFailedToDecryptSegment ||
evidence.mediaErrorCode === VhsPlaybackMediaErrorCode.Encrypted
) {
return DiagnosticCode.DrmOrEncryption;
}
if (
evidence.mediaErrorCode ===
VhsPlaybackMediaErrorCode.SourceNotSupported &&
isLikelyContainerIssue(metadata)
) {
return DiagnosticCode.UnsupportedContainer;
}
return DiagnosticCode.UnknownPlaybackError;
}
function getNativeHttpStatus(status: unknown): number | undefined {
return typeof status === 'number' &&
Number.isInteger(status) &&
status >= 400 &&
status <= 599
? status
: undefined;
}
function getNativeErrorType(errorType: unknown): string | undefined {
return typeof errorType === 'string' &&
NATIVE_ERROR_TYPE_PATTERN.test(errorType)
? errorType
: undefined;
}
function hasCodecs(metadata: PlaybackSourceMetadata): boolean {
return metadata.audioCodecs.length > 0 || metadata.videoCodecs.length > 0;
}