fix(playback): structure mpegts error evidence

This commit is contained in:
4gray committed 2026-08-01 16:20:18 +02:00
1 parent 0c377e3ef5
commit fc8fc4bdae
7 files changed
+533 -158

No files matched your search

@@ -0,0 +1,64 @@
export const MpegTsPlaybackEngineType = {
Network: 'NetworkError',
Media: 'MediaError',
Other: 'OtherError',
Unknown: 'unknown',
} as const;
export type MpegTsPlaybackEngineType =
(typeof MpegTsPlaybackEngineType)[keyof typeof MpegTsPlaybackEngineType];
export const MpegTsPlaybackEngineDetails = {
NetworkException: 'Exception',
HttpStatusCodeInvalid: 'HttpStatusCodeInvalid',
ConnectingTimeout: 'ConnectingTimeout',
UnrecoverableEarlyEof: 'UnrecoverableEarlyEof',
MediaMseError: 'MediaMSEError',
FormatError: 'FormatError',
FormatUnsupported: 'FormatUnsupported',
CodecUnsupported: 'CodecUnsupported',
Unknown: 'unknown',
} as const;
export type MpegTsPlaybackEngineDetails =
(typeof MpegTsPlaybackEngineDetails)[keyof typeof MpegTsPlaybackEngineDetails];
export const MpegTsPlaybackDisposition = {
Terminal: 'terminal',
} as const;
export type MpegTsPlaybackDisposition =
(typeof MpegTsPlaybackDisposition)[keyof typeof MpegTsPlaybackDisposition];
export const MpegTsPlaybackStage = {
Loader: 'loader',
Demux: 'demux',
MediaSource: 'media-source',
Unknown: 'unknown',
} as const;
export type MpegTsPlaybackStage =
(typeof MpegTsPlaybackStage)[keyof typeof MpegTsPlaybackStage];
export const MpegTsPlaybackFailure = {
Http: 'http',
Timeout: 'timeout',
Network: 'network',
TruncatedStream: 'truncated-stream',
Format: 'format',
Codec: 'codec',
MediaSource: 'media-source',
Unknown: 'unknown',
} as const;
export type MpegTsPlaybackFailure =
(typeof MpegTsPlaybackFailure)[keyof typeof MpegTsPlaybackFailure];
export interface MpegTsPlaybackEvidence {
readonly engineType: MpegTsPlaybackEngineType;
readonly engineDetails: MpegTsPlaybackEngineDetails;
readonly disposition: MpegTsPlaybackDisposition;
readonly stage: MpegTsPlaybackStage;
readonly failure: MpegTsPlaybackFailure;
readonly httpStatus?: number;
}
@@ -0,0 +1,239 @@
import mpegts from 'mpegts.js';
import {
InlinePlaybackPlayer,
PlaybackDiagnosticCode,
classifyMpegTsPlaybackIssue,
createPlaybackSourceMetadata,
} from './playback-diagnostics.util';
import {
MpegTsPlaybackEngineDetails,
MpegTsPlaybackEngineType,
} from './mpegts-playback-evidence.model';
import {
MPEGTS_DIAGNOSTIC_VERSION,
createMpegTsPlaybackEvidence,
} from './mpegts-playback-evidence.util';
const METADATA = createPlaybackSourceMetadata({
url: 'https://example.test/live.ts',
mimeType: 'video/mp2t',
player: InlinePlaybackPlayer.VideoJs,
});
describe('mpegts.js playback evidence', () => {
it('locks the accepted public contract to mpegts.js 1.8.0', () => {
expect(mpegts.version).toBe(MPEGTS_DIAGNOSTIC_VERSION);
expect(mpegts.ErrorTypes).toEqual({
NETWORK_ERROR: MpegTsPlaybackEngineType.Network,
MEDIA_ERROR: MpegTsPlaybackEngineType.Media,
OTHER_ERROR: MpegTsPlaybackEngineType.Other,
});
expect(mpegts.ErrorDetails).toEqual({
NETWORK_EXCEPTION:
MpegTsPlaybackEngineDetails.NetworkException,
NETWORK_STATUS_CODE_INVALID:
MpegTsPlaybackEngineDetails.HttpStatusCodeInvalid,
NETWORK_TIMEOUT: MpegTsPlaybackEngineDetails.ConnectingTimeout,
NETWORK_UNRECOVERABLE_EARLY_EOF:
MpegTsPlaybackEngineDetails.UnrecoverableEarlyEof,
MEDIA_MSE_ERROR: MpegTsPlaybackEngineDetails.MediaMseError,
MEDIA_FORMAT_ERROR: MpegTsPlaybackEngineDetails.FormatError,
MEDIA_FORMAT_UNSUPPORTED:
MpegTsPlaybackEngineDetails.FormatUnsupported,
MEDIA_CODEC_UNSUPPORTED:
MpegTsPlaybackEngineDetails.CodecUnsupported,
});
});
it.each([
[
'NetworkError',
'HttpStatusCodeInvalid',
'loader',
'http',
],
['NetworkError', 'ConnectingTimeout', 'loader', 'timeout'],
['NetworkError', 'Exception', 'loader', 'network'],
[
'NetworkError',
'UnrecoverableEarlyEof',
'loader',
'truncated-stream',
],
['MediaError', 'FormatError', 'demux', 'format'],
['MediaError', 'FormatUnsupported', 'demux', 'format'],
['MediaError', 'CodecUnsupported', 'demux', 'codec'],
[
'MediaError',
'MediaMSEError',
'media-source',
'media-source',
],
])(
'maps %s + %s to %s/%s',
(engineType, engineDetails, stage, failure) => {
expect(
createMpegTsPlaybackEvidence(
engineType,
engineDetails,
undefined
)
).toEqual({
engineType,
engineDetails,
disposition: 'terminal',
stage,
failure,
});
}
);
it.each([
['OtherError', 'CodecUnsupported'],
['NetworkError', 'CodecUnsupported'],
['MediaError', 'ConnectingTimeout'],
['networkerror', 'HttpStatusCodeInvalid'],
['NetworkError', 'httpstatuscodeinvalid'],
[{ type: 'NetworkError' }, 'Exception'],
['MediaError', { details: 'FormatError' }],
])('keeps inconsistent or malformed %p + %p evidence unknown', (type, details) => {
const evidence = createMpegTsPlaybackEvidence(type, details, {
code: 503,
});
expect(evidence.stage).toBe('unknown');
expect(evidence.failure).toBe('unknown');
expect(evidence.httpStatus).toBeUndefined();
});
it.each([399, 600, 404.5, '404', null, undefined])(
'rejects invalid HTTP status %p',
(code) => {
const evidence = createMpegTsPlaybackEvidence(
'NetworkError',
'HttpStatusCodeInvalid',
{ code }
);
expect(evidence.httpStatus).toBeUndefined();
}
);
it('reads HTTP status only from the exact top-level public slot', () => {
expect(
createMpegTsPlaybackEvidence(
'NetworkError',
'HttpStatusCodeInvalid',
{ code: 404 }
).httpStatus
).toBe(404);
expect(
createMpegTsPlaybackEvidence('NetworkError', 'Exception', {
code: 503,
}).httpStatus
).toBeUndefined();
expect(
createMpegTsPlaybackEvidence(
'NetworkError',
'HttpStatusCodeInvalid',
{ response: { code: 503 } }
).httpStatus
).toBeUndefined();
});
it('does not inspect or retain arbitrary engine information', () => {
const secret = 'mpegts-info-secret';
const circularInfo: Record<string, unknown> = {
code: 404,
msg: `Not Found ${secret}`,
url: `https://provider.example/error?token=${secret}`,
headers: { Authorization: secret },
};
circularInfo['self'] = circularInfo;
const evidence = createMpegTsPlaybackEvidence(
'NetworkError',
'HttpStatusCodeInvalid',
circularInfo
);
expect(evidence.httpStatus).toBe(404);
expect(JSON.stringify(evidence)).not.toContain(secret);
expect(JSON.stringify(evidence)).not.toContain('provider.example');
});
it.each([
['NetworkError', 'HttpStatusCodeInvalid', 'network-error', false],
['NetworkError', 'ConnectingTimeout', 'network-error', false],
['NetworkError', 'Exception', 'network-error', false],
[
'NetworkError',
'UnrecoverableEarlyEof',
'media-decode-error',
true,
],
['MediaError', 'FormatError', 'media-decode-error', true],
[
'MediaError',
'FormatUnsupported',
'unsupported-container',
true,
],
[
'MediaError',
'CodecUnsupported',
'unsupported-codec',
true,
],
['MediaError', 'MediaMSEError', 'media-decode-error', true],
['OtherError', 'Exception', 'unknown-playback-error', false],
])(
'classifies %s + %s as %s with fallback=%s',
(type, details, code, externalFallbackRecommended) => {
const issue = classifyMpegTsPlaybackIssue(
createMpegTsPlaybackEvidence(type, details, undefined),
METADATA
);
expect(issue.code).toBe(code);
expect(issue.externalFallbackRecommended).toBe(
externalFallbackRecommended
);
expect(issue.details).toBeUndefined();
}
);
it('preserves an exact HTTP failure without retaining provider details', () => {
const secret = 'mpegts-http-secret';
const issue = classifyMpegTsPlaybackIssue(
createMpegTsPlaybackEvidence(
'NetworkError',
'HttpStatusCodeInvalid',
{
code: 404,
msg: `Not Found ${secret}`,
url: `https://provider.example/error?token=${secret}`,
}
),
METADATA
);
expect(issue).toEqual(
expect.objectContaining({
code: PlaybackDiagnosticCode.NetworkError,
httpStatus: 404,
mpegTs: expect.objectContaining({
engineType: 'NetworkError',
engineDetails: 'HttpStatusCodeInvalid',
disposition: 'terminal',
stage: 'loader',
failure: 'http',
httpStatus: 404,
}),
externalFallbackRecommended: false,
})
);
expect(issue.details).toBeUndefined();
expect(JSON.stringify(issue)).not.toContain(secret);
});
});
@@ -0,0 +1,167 @@
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;
}
@@ -3,6 +3,9 @@ import type {
ResolvedPortalPlayback,
} from '@iptvnator/shared/interfaces';
import type { ErrorDetails, ErrorTypes } from 'hls.js';
import type { MpegTsPlaybackEvidence } from './mpegts-playback-evidence.model';
export * from './mpegts-playback-evidence.model';
export const PlaybackDiagnosticCode = {
UnsupportedContainer: 'unsupported-container',
@@ -243,13 +246,6 @@ export interface ShakaPlaybackEvidence {
readonly httpStatus?: number;
}
export interface MpegTsPlaybackErrorInput {
readonly type?: string;
readonly details?: string;
readonly message?: string;
readonly info?: unknown;
}
export interface PlaybackDiagnostic {
readonly code: PlaybackDiagnosticCode;
readonly source: PlaybackDiagnosticSource;
@@ -266,6 +262,7 @@ export interface PlaybackDiagnostic {
readonly nativeErrorType?: string;
readonly vhs?: VhsPlaybackEvidence;
readonly hls?: HlsPlaybackEvidence;
readonly mpegTs?: MpegTsPlaybackEvidence;
readonly shaka?: ShakaPlaybackEvidence;
readonly externalFallbackRecommended: boolean;
}
@@ -10,6 +10,7 @@ import {
classifyHlsPlaybackIssue,
classifyMpegTsPlaybackIssue,
classifyNativePlaybackIssue,
createMpegTsPlaybackEvidence,
createPlaybackSourceMetadata,
getLikelyBrowserUnsupportedCodecLabels,
getPlaybackMediaExtensionFromUrl,
@@ -584,13 +585,12 @@ describe('playback diagnostics', () => {
expect(issue.externalFallbackRecommended).toBe(false);
});
it('classifies mpegts browser fetch restrictions separately from generic network errors', () => {
it('does not guess mpegts browser access from exception messages', () => {
const secret = 'blocked by CORS policy';
const issue = classifyMpegTsPlaybackIssue(
{
type: 'NetworkError',
details:
'Fetch blocked by access-control policy while loading segment',
},
createMpegTsPlaybackEvidence('NetworkError', 'Exception', {
msg: secret,
}),
createPlaybackSourceMetadata({
url: 'https://provider.example/live/channel.ts',
mimeType: 'video/mp2t',
@@ -598,17 +598,18 @@ describe('playback diagnostics', () => {
})
);
expect(issue.code).toBe('browser-access-error');
expect(issue.externalFallbackRecommended).toBe(true);
expect(issue.code).toBe(PlaybackDiagnosticCode.NetworkError);
expect(issue.externalFallbackRecommended).toBe(false);
expect(JSON.stringify(issue)).not.toContain(secret);
});
it('classifies mpegts early EOF failures as fallback-actionable media errors', () => {
const issue = classifyMpegTsPlaybackIssue(
{
type: 'NetworkError',
details: 'UnrecoverableEarlyEof',
info: { msg: 'Fetch stream meet Early-EOF' },
},
createMpegTsPlaybackEvidence(
'NetworkError',
'UnrecoverableEarlyEof',
{ msg: 'Fetch stream meet Early-EOF' }
),
createPlaybackSourceMetadata({
url: 'https://provider.example/movie/123.ts',
mimeType: 'video/mp2t',
@@ -618,15 +619,17 @@ describe('playback diagnostics', () => {
expect(issue.code).toBe(PlaybackDiagnosticCode.MediaDecodeError);
expect(issue.externalFallbackRecommended).toBe(true);
expect(issue.details).toContain('Early-EOF');
expect(issue.mpegTs?.failure).toBe('truncated-stream');
expect(issue.details).toBeUndefined();
});
it('classifies mpegts codec errors as unsupported codec fallbacks', () => {
const issue = classifyMpegTsPlaybackIssue(
{
type: 'MediaError',
details: 'MediaCodecUnsupported',
},
createMpegTsPlaybackEvidence(
'MediaError',
'CodecUnsupported',
undefined
),
createPlaybackSourceMetadata({
url: 'https://example.com/live/channel.ts',
mimeType: 'video/mp2t',
@@ -1,6 +1,6 @@
import type {
HlsPlaybackEvidence,
MpegTsPlaybackErrorInput,
MpegTsPlaybackEvidence,
NativePlaybackErrorInput,
PlaybackDiagnostic,
PlaybackDiagnosticCode,
@@ -12,23 +12,24 @@ import type {
} 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,
isEarlyEofFailure,
isNetworkFailure,
normalizeErrorDetails,
} from './playback-error-patterns.util';
import { createMpegTsPlaybackEvidence } from './mpegts-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,
@@ -163,65 +164,15 @@ export function classifyHlsPlaybackIssue(
}
export function classifyMpegTsPlaybackIssue(
error: MpegTsPlaybackErrorInput,
evidence: MpegTsPlaybackEvidence,
metadata: PlaybackSourceMetadata
): PlaybackDiagnostic {
const details = normalizeErrorDetails(error);
const lowerDetails = details.toLowerCase();
const lowerType = (error.type ?? '').toLowerCase();
if (isEarlyEofFailure(lowerDetails)) {
return createPlaybackDiagnostic({
code: DiagnosticCode.MediaDecodeError,
source: DiagnosticSource.MpegTs,
metadata,
details,
});
}
if (isNetworkFailure(lowerType, lowerDetails)) {
return createPlaybackDiagnostic({
code: isBrowserAccessFailure(lowerDetails)
? DiagnosticCode.BrowserAccessError
: DiagnosticCode.NetworkError,
source: DiagnosticSource.MpegTs,
metadata,
details,
});
}
if (lowerDetails.includes('codec')) {
return createPlaybackDiagnostic({
code: DiagnosticCode.UnsupportedCodec,
source: DiagnosticSource.MpegTs,
metadata,
details,
});
}
if (lowerDetails.includes('format') || lowerDetails.includes('mse')) {
return createPlaybackDiagnostic({
code: DiagnosticCode.UnsupportedContainer,
source: DiagnosticSource.MpegTs,
metadata,
details,
});
}
if (lowerType.includes('media')) {
return createPlaybackDiagnostic({
code: DiagnosticCode.MediaDecodeError,
source: DiagnosticSource.MpegTs,
metadata,
details,
});
}
return createPlaybackDiagnostic({
code: DiagnosticCode.UnknownPlaybackError,
code: getMpegTsPlaybackDiagnosticCode(evidence),
source: DiagnosticSource.MpegTs,
metadata,
details,
httpStatus: evidence.httpStatus,
mpegTs: evidence,
});
}
@@ -258,6 +209,7 @@ export function createPlaybackDiagnostic(options: {
readonly nativeErrorType?: string;
readonly vhs?: VhsPlaybackEvidence;
readonly hls?: HlsPlaybackEvidence;
readonly mpegTs?: MpegTsPlaybackEvidence;
readonly shaka?: ShakaPlaybackEvidence;
/** Overrides the code-derived recommendation, e.g. when external players
* are known to be unable to handle the stream either. */
@@ -274,6 +226,7 @@ export function createPlaybackDiagnostic(options: {
nativeErrorType,
vhs,
hls,
mpegTs,
shaka,
} = options;
@@ -293,6 +246,7 @@ export function createPlaybackDiagnostic(options: {
nativeErrorType,
vhs,
hls,
mpegTs,
shaka,
externalFallbackRecommended:
options.externalFallbackRecommended ??
@@ -300,6 +254,29 @@ export function createPlaybackDiagnostic(options: {
};
}
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
@@ -1,25 +1,3 @@
import type { MpegTsPlaybackErrorInput } from './playback-diagnostics.model';
export function normalizeErrorDetails(
error: MpegTsPlaybackErrorInput
): string {
const info = normalizeErrorPayload(error.info);
return [error.details, error.message, info]
.filter((part): part is string => Boolean(part))
.join(' ');
}
export function isNetworkFailure(type: string, details: string): boolean {
return (
type.includes('network') ||
details.includes('network') ||
details.includes('loaderror') ||
details.includes('timeout') ||
details.includes('status')
);
}
export function isBrowserAccessFailure(details: string): boolean {
return (
details.includes('cors') ||
@@ -38,53 +16,3 @@ export function isBrowserAccessFailure(details: string): boolean {
details.includes('err_cleartext')
);
}
export function isEarlyEofFailure(details: string): boolean {
const compactDetails = details.replace(/[^a-z0-9]/g, '');
return compactDetails.includes('earlyeof');
}
export function isCodecFailure(details: string): boolean {
return (
details.includes('codec') ||
details.includes('incompatiblecodecs') ||
details.includes('addcodec')
);
}
export function isDrmOrEncryptionFailure(details: string): boolean {
return (
details.includes('decrypt') ||
details.includes('keysystem') ||
details.includes('keyload') ||
details.includes('license') ||
details.includes('drm')
);
}
function normalizeErrorPayload(payload: unknown): string {
if (!payload) {
return '';
}
if (typeof payload === 'string') {
return payload;
}
if (payload instanceof Error) {
const extraDetails = stringifyUnknown(payload);
return [payload.message, extraDetails === '{}' ? '' : extraDetails]
.filter(Boolean)
.join(' ');
}
return stringifyUnknown(payload);
}
function stringifyUnknown(value: unknown): string {
try {
return JSON.stringify(value) || '';
} catch {
return String(value);
}
}