From 2e3cde484d5f04630429a4d31b9769a4db625588 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 1 Aug 2026 12:01:51 +0200 Subject: [PATCH] fix(playback): structure HTML5 mpegts errors --- .../html-video-player-diagnostics.ts | 9 +++- ....component.shared-controls.sources.spec.ts | 46 +++++++++++++++++++ ...component.shared-controls.spec-fixtures.ts | 20 +++++++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/libs/ui/playback/src/lib/html-video-player/html-video-player-diagnostics.ts b/libs/ui/playback/src/lib/html-video-player/html-video-player-diagnostics.ts index 3d4ffa8bd..048176b95 100644 --- a/libs/ui/playback/src/lib/html-video-player/html-video-player-diagnostics.ts +++ b/libs/ui/playback/src/lib/html-video-player/html-video-player-diagnostics.ts @@ -7,6 +7,7 @@ import { classifyMpegTsPlaybackIssue, classifyUnsupportedHlsManifestCodecs, createHlsPlaybackEvidence, + createMpegTsPlaybackEvidence, createPlaybackSourceMetadata, } from '../playback-diagnostics/playback-diagnostics.util'; @@ -69,12 +70,16 @@ export function emitFatalHlsPlaybackError( export function emitMpegTsPlaybackError( url: string, - error: { type: string; details: string; info: unknown }, + error: { type: unknown; details: unknown; info: unknown }, emitPlaybackIssue: (issue: PlaybackDiagnostic) => void ): void { emitPlaybackIssue( classifyMpegTsPlaybackIssue( - error, + createMpegTsPlaybackEvidence( + error.type, + error.details, + error.info + ), createHtml5SourceMetadata(url, 'video/mp2t') ) ); diff --git a/libs/ui/playback/src/lib/html-video-player/html-video-player.component.shared-controls.sources.spec.ts b/libs/ui/playback/src/lib/html-video-player/html-video-player.component.shared-controls.sources.spec.ts index 929772534..ac5f748d5 100644 --- a/libs/ui/playback/src/lib/html-video-player/html-video-player.component.shared-controls.sources.spec.ts +++ b/libs/ui/playback/src/lib/html-video-player/html-video-player.component.shared-controls.sources.spec.ts @@ -114,6 +114,52 @@ describe('HtmlVideoPlayerComponent shared controls sources', () => { } ); + it('emits structured HTTP evidence for mpegts.js failures', () => { + mpegTsIsSupported.mockReturnValue(true); + const { component } = renderSharedControls( + HtmlVideoPlayerComponent, + fixtures, + { + channel: { + ...TEST_CHANNEL, + url: 'https://example.test/missing.ts', + }, + } + ); + const issues: unknown[] = []; + component.playbackIssue.subscribe((issue) => issues.push(issue)); + const secret = 'html-mpegts-secret'; + + mpegTsInstances[0].emit( + 'error', + 'NetworkError', + 'HttpStatusCodeInvalid', + { + code: 404, + msg: `Not Found ${secret}`, + url: `https://provider.example/error?token=${secret}`, + } + ); + + expect(issues).toEqual([ + expect.objectContaining({ + code: 'network-error', + source: 'mpegts', + sourceUrl: 'https://example.test/missing.ts', + player: 'html5', + httpStatus: 404, + mpegTs: expect.objectContaining({ + engineType: 'NetworkError', + engineDetails: 'HttpStatusCodeInvalid', + failure: 'http', + httpStatus: 404, + }), + externalFallbackRecommended: false, + }), + ]); + expect(JSON.stringify(issues)).not.toContain(secret); + }); + it('owns one MPEG-TS source between media attachment and loading', () => { mpegTsIsSupported.mockReturnValue(true); const { component } = renderSharedControls( diff --git a/libs/ui/playback/src/lib/html-video-player/html-video-player.component.shared-controls.spec-fixtures.ts b/libs/ui/playback/src/lib/html-video-player/html-video-player.component.shared-controls.spec-fixtures.ts index 65bce6152..006f4d97b 100644 --- a/libs/ui/playback/src/lib/html-video-player/html-video-player.component.shared-controls.spec-fixtures.ts +++ b/libs/ui/playback/src/lib/html-video-player/html-video-player.component.shared-controls.spec-fixtures.ts @@ -93,10 +93,22 @@ export class MockHls { } export class MockMpegTsPlayer { + private readonly listeners = new Map< + string, + Set<(...args: unknown[]) => void> + >(); readonly attachMediaElement = jest.fn(() => lifecycle.push('mpegts:attachMedia') ); - readonly on = jest.fn(); + readonly on = jest.fn( + (event: string, listener: (...args: unknown[]) => void) => { + const listeners = + this.listeners.get(event) ?? + new Set<(...args: unknown[]) => void>(); + listeners.add(listener); + this.listeners.set(event, listeners); + } + ); readonly load = jest.fn(() => lifecycle.push('mpegts:load')); readonly pause = jest.fn(); readonly unload = jest.fn(); @@ -106,6 +118,12 @@ export class MockMpegTsPlayer { constructor() { mpegTsInstances.push(this); } + + emit(event: string, ...args: unknown[]): void { + for (const listener of this.listeners.get(event) ?? []) { + listener(...args); + } + } } export const mpegTsCreatePlayer = jest.fn(() => new MockMpegTsPlayer());