fix(playback): share mpegts evidence across players

This commit is contained in:
4gray committed 2026-08-01 16:20:18 +02:00
1 parent 2e3cde484d
commit dfd7ac84d4
5 files changed
+77 -25

No files matched your search

@@ -186,10 +186,11 @@ describe('ArtPlayerSourceSession', () => {
details: 'stale callback',
fatal: true,
});
const secret = 'art-mpegts-secret';
mpegTsInstances.at(-1)?.handlers.get('error')?.(
'mediaError',
'unsupported codec',
{}
'MediaError',
'CodecUnsupported',
{ message: secret, headers: { Authorization: secret } }
);
expect(emitted).toEqual([
@@ -199,11 +200,18 @@ describe('ArtPlayerSourceSession', () => {
player: 'artplayer',
}),
expect.objectContaining({
code: 'unsupported-codec',
source: 'mpegts',
sourceUrl: 'https://example.test/live.ts',
player: 'artplayer',
mpegTs: expect.objectContaining({
engineType: 'MediaError',
engineDetails: 'CodecUnsupported',
failure: 'codec',
}),
}),
]);
expect(JSON.stringify(emitted)).not.toContain(secret);
});
it('reports only structured evidence for a fatal HLS manifest HTTP failure', () => {
@@ -10,6 +10,7 @@ import {
classifyMpegTsPlaybackIssue,
classifyUnsupportedHlsManifestCodecs,
createHlsPlaybackEvidence,
createMpegTsPlaybackEvidence,
createPlaybackSourceMetadata,
} from '../playback-diagnostics/playback-diagnostics.util';
import type { WebVideoControlsAdapter } from '../player-controls';
@@ -66,7 +67,7 @@ export class ArtPlayerSourceSession {
| null = null;
private mpegTsPlayer: mpegts.Player | null = null;
private mpegTsErrorListener:
| ((type: string, details: string, info: unknown) => void)
| ((type: unknown, details: unknown, info: unknown) => void)
| null = null;
private shakaSession: ShakaVideoSession | null = null;
private destroyed = false;
@@ -192,7 +193,7 @@ export class ArtPlayerSourceSession {
}
this.config.emitPlaybackIssue(
classifyMpegTsPlaybackIssue(
{ type, details, info },
createMpegTsPlaybackEvidence(type, details, info),
this.createSourceMetadata(url, 'video/mp2t')
)
);
@@ -239,10 +239,11 @@ describe('ArtPlayerComponent', () => {
artPlayerInstances[0].video,
'https://example.com/live/channel.ts'
);
const secret = 'art-component-mpegts-secret';
mpegTsInstances[0].handlers.get('error')?.(
'mediaError',
'unsupported codec',
{}
'MediaError',
'CodecUnsupported',
{ message: secret }
);
expect(issues).toEqual([
@@ -250,9 +251,15 @@ describe('ArtPlayerComponent', () => {
code: 'unsupported-codec',
source: 'mpegts',
sourceUrl: 'https://example.com/live/channel.ts',
mpegTs: expect.objectContaining({
engineType: 'MediaError',
engineDetails: 'CodecUnsupported',
failure: 'codec',
}),
externalFallbackRecommended: true,
}),
]);
expect(JSON.stringify(issues)).not.toContain(secret);
});
it('emits playbackEnded exactly once for a native ended event and not during reload or destroy', () => {
@@ -111,28 +111,68 @@ describe('VjsMpegTsSession', () => {
expect(duration).toHaveBeenCalledWith(200);
});
it('classifies engine errors with Video.js source metadata', () => {
it('emits structured HTTP evidence with Video.js source metadata', () => {
const mpegTsPlayer = createMpegTsPlayer();
createPlayerMock.mockReturnValue(mpegTsPlayer);
const video = document.createElement('video');
const { session, emitPlaybackIssue } = createSession();
const secret = 'vjs-mpegts-secret';
session.start('https://example.test/live/stream.ts', video);
mpegTsPlayer.emit(
'error',
'NetworkError',
'HttpStatusCodeInvalid',
{
code: 503,
msg: `Service Unavailable ${secret}`,
headers: { Authorization: secret },
}
);
expect(emitPlaybackIssue).toHaveBeenCalledWith(
expect.objectContaining({
code: 'network-error',
source: 'mpegts',
sourceUrl: 'https://example.test/live/stream.ts',
player: 'videojs',
httpStatus: 503,
mpegTs: expect.objectContaining({
engineType: 'NetworkError',
engineDetails: 'HttpStatusCodeInvalid',
failure: 'http',
httpStatus: 503,
}),
externalFallbackRecommended: false,
})
);
expect(JSON.stringify(emitPlaybackIssue.mock.calls)).not.toContain(
secret
);
});
it('does not guess browser access from a generic mpegts exception', () => {
const mpegTsPlayer = createMpegTsPlayer();
createPlayerMock.mockReturnValue(mpegTsPlayer);
const video = document.createElement('video');
const { session, emitPlaybackIssue } = createSession();
session.start('https://example.test/live/stream.ts', video);
mpegTsPlayer.emit(
'error',
'NetworkError',
'FetchError',
new Error('CORS blocked')
);
mpegTsPlayer.emit('error', 'NetworkError', 'Exception', {
code: -1,
msg: 'blocked by CORS policy',
});
expect(emitPlaybackIssue).toHaveBeenCalledWith(
expect.objectContaining({
code: 'browser-access-error',
source: 'mpegts',
sourceUrl: 'https://example.test/live/stream.ts',
player: 'videojs',
code: 'network-error',
mpegTs: expect.objectContaining({ failure: 'network' }),
externalFallbackRecommended: false,
})
);
expect(JSON.stringify(emitPlaybackIssue.mock.calls)).not.toContain(
'CORS'
);
});
it('replaces an active session and removes listeners from the old video', () => {
@@ -3,6 +3,7 @@ import {
InlinePlaybackPlayer,
type PlaybackDiagnostic,
classifyMpegTsPlaybackIssue,
createMpegTsPlaybackEvidence,
createPlaybackSourceMetadata,
getPlaybackMediaExtensionFromUrl,
} from '../playback-diagnostics/playback-diagnostics.util';
@@ -59,12 +60,7 @@ export class VjsMpegTsSession {
this.syncDuration();
this.config.emitPlaybackIssue(
classifyMpegTsPlaybackIssue(
{
type: typeof type === 'string' ? type : undefined,
details:
typeof details === 'string' ? details : undefined,
info,
},
createMpegTsPlaybackEvidence(type, details, info),
createPlaybackSourceMetadata({
url,
mimeType: 'video/mp2t',