From dfd7ac84d4d423467034b4a25f25aa59efa8d408 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 1 Aug 2026 12:03:07 +0200 Subject: [PATCH] fix(playback): share mpegts evidence across players --- .../art-player-source-session.spec.ts | 14 ++++- .../art-player/art-player-source-session.ts | 5 +- .../art-player/art-player.component.spec.ts | 13 +++- .../lib/vjs-player/vjs-mpegts-session.spec.ts | 62 +++++++++++++++---- .../src/lib/vjs-player/vjs-mpegts-session.ts | 8 +-- 5 files changed, 77 insertions(+), 25 deletions(-) diff --git a/libs/ui/playback/src/lib/art-player/art-player-source-session.spec.ts b/libs/ui/playback/src/lib/art-player/art-player-source-session.spec.ts index 3533dfd45..fcf24cf3b 100644 --- a/libs/ui/playback/src/lib/art-player/art-player-source-session.spec.ts +++ b/libs/ui/playback/src/lib/art-player/art-player-source-session.spec.ts @@ -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', () => { diff --git a/libs/ui/playback/src/lib/art-player/art-player-source-session.ts b/libs/ui/playback/src/lib/art-player/art-player-source-session.ts index b5cda67c8..5c71da94b 100644 --- a/libs/ui/playback/src/lib/art-player/art-player-source-session.ts +++ b/libs/ui/playback/src/lib/art-player/art-player-source-session.ts @@ -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') ) ); diff --git a/libs/ui/playback/src/lib/art-player/art-player.component.spec.ts b/libs/ui/playback/src/lib/art-player/art-player.component.spec.ts index 8ad9a83b7..60f0f0bcb 100644 --- a/libs/ui/playback/src/lib/art-player/art-player.component.spec.ts +++ b/libs/ui/playback/src/lib/art-player/art-player.component.spec.ts @@ -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', () => { diff --git a/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.spec.ts b/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.spec.ts index d472e0e80..e8603a3ad 100644 --- a/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.spec.ts +++ b/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.spec.ts @@ -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', () => { diff --git a/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.ts b/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.ts index 6ee7e4fc0..3e272eb0e 100644 --- a/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.ts +++ b/libs/ui/playback/src/lib/vjs-player/vjs-mpegts-session.ts @@ -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',