fix(playback): tighten browser access diagnostics

Entire-Checkpoint: f957cd9849e0
This commit is contained in:
4gray committed 2026-05-14 22:23:01 +02:00
1 parent d71d21f4d1
commit 48f16c7148
5 files changed
+129 -15

No files matched your search

@@ -279,6 +279,7 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
details: data.details,
fatal: data.fatal,
message: data.error?.message,
error: data.error,
},
this.createSourceMetadata(url, 'application/x-mpegURL')
)
@@ -109,4 +109,36 @@ describe('HtmlVideoPlayerComponent', () => {
expect(issues).toEqual([]);
});
it('keeps raw HLS error object context in emitted playback issue details', () => {
const issues: Array<{ details?: string }> = [];
component.playbackIssue.subscribe((issue) => {
if (issue) issues.push(issue);
});
(
component as unknown as {
handleHlsError: (
url: string,
data: {
type: string;
details: string;
fatal: boolean;
error?: unknown;
}
) => void;
}
).handleHlsError('https://example.com/live/playlist.m3u8', {
type: 'networkError',
details: 'manifestLoadError',
fatal: true,
error: {
context: 'xhr setup failed',
status: 0,
},
});
expect(issues[0].details).toContain('xhr setup failed');
expect(issues[0].details).toContain('"status":0');
});
});
@@ -288,6 +288,7 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
details: data.details,
fatal: data.fatal,
message: data.error?.message,
error: data.error,
},
this.createSourceMetadata(url, 'application/x-mpegURL')
)
@@ -74,6 +74,20 @@ describe('playback diagnostics', () => {
expect(unsupportedSourceIssue.externalFallbackRecommended).toBe(true);
});
it('classifies MIME-only unsupported containers as container diagnostics', () => {
const issue = classifyNativePlaybackIssue(
{ code: 4, message: 'source not supported' },
createPlaybackSourceMetadata({
url: 'https://example.com/archive/stream',
mimeType: 'video/x-msvideo',
player: 'html5',
})
);
expect(issue.code).toBe(PlaybackDiagnosticCode.UnsupportedContainer);
expect(issue.container).toBe('x-msvideo');
});
it('classifies HLS network errors without claiming codec incompatibility', () => {
const issue = classifyHlsPlaybackIssue(
{
@@ -91,6 +105,44 @@ describe('playback diagnostics', () => {
expect(issue.externalFallbackRecommended).toBe(false);
});
it('does not treat provider-side blocked messages as browser access errors', () => {
const issue = classifyHlsPlaybackIssue(
{
type: 'networkError',
details: 'manifestLoadError Request blocked by rate limiter',
fatal: true,
},
createPlaybackSourceMetadata({
url: 'https://provider.example/live/index.m3u8',
player: 'videojs',
})
);
expect(issue.code).toBe(PlaybackDiagnosticCode.NetworkError);
expect(issue.externalFallbackRecommended).toBe(false);
});
it('keeps raw HLS error object context in diagnostic details', () => {
const issue = classifyHlsPlaybackIssue(
{
type: 'networkError',
details: 'manifestLoadError',
fatal: true,
error: {
context: 'xhr setup failed',
status: 0,
},
},
createPlaybackSourceMetadata({
url: 'https://provider.example/live/index.m3u8',
player: 'videojs',
})
);
expect(issue.details).toContain('xhr setup failed');
expect(issue.details).toContain('"status":0');
});
it('classifies HLS browser access blocks separately from provider network failures', () => {
const issue = classifyHlsPlaybackIssue(
{
@@ -115,6 +115,15 @@ const UNSUPPORTED_CONTAINER_EXTENSIONS = new Set([
'wmv',
]);
const UNSUPPORTED_CONTAINER_NAMES = new Set([
...UNSUPPORTED_CONTAINER_EXTENSIONS,
'matroska',
'mp2t',
'quicktime',
'x-matroska',
'x-msvideo',
]);
const DECLARED_MEDIA_EXTENSION_QUERY_KEYS = [
'extension',
'ext',
@@ -525,7 +534,8 @@ function inferContainerFromMimeType(mimeType: string | undefined): string {
function isLikelyContainerIssue(metadata: PlaybackSourceMetadata): boolean {
return (
UNSUPPORTED_CONTAINER_EXTENSIONS.has(metadata.extension) ||
UNSUPPORTED_CONTAINER_NAMES.has(metadata.extension) ||
UNSUPPORTED_CONTAINER_NAMES.has(metadata.container) ||
metadata.mimeType === 'video/matroska'
);
}
@@ -535,24 +545,41 @@ function normalizeErrorDetails(
): string {
const hlsError = 'error' in error ? error.error : undefined;
const mpegTsInfo = 'info' in error ? error.info : undefined;
const errorMessage =
hlsError instanceof Error
? hlsError.message
: typeof hlsError === 'string'
? hlsError
: '';
const info =
typeof mpegTsInfo === 'string'
? mpegTsInfo
: mpegTsInfo
? JSON.stringify(mpegTsInfo)
: '';
const errorMessage = normalizeErrorPayload(hlsError);
const info = normalizeErrorPayload(mpegTsInfo);
return [error.details, error.message, errorMessage, info]
.filter((part): part is string => Boolean(part))
.join(' ');
}
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);
}
}
function isNetworkFailure(type: string, details: string): boolean {
return (
type.includes('network') ||
@@ -573,8 +600,9 @@ function isBrowserAccessFailure(details: string): boolean {
details.includes('content security policy') ||
details.includes('mixed content') ||
details.includes('private network access') ||
details.includes('blocked by') ||
details.includes('has been blocked') ||
details.includes('blocked by content security') ||
details.includes('blocked by cors') ||
details.includes('blocked by mixed content') ||
details.includes('not allowed to load local resource') ||
details.includes('err_blocked') ||
details.includes('err_cleartext')