From 7a32fa2e4dc2e454742022c19a31a7385ec9f256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Tue, 24 Feb 2026 16:37:07 +0100 Subject: [PATCH] fix: Handle ID3 EMSG duration according to AOM spec (#9757) Fixes https://github.com/shaka-project/shaka-player/issues/9753 --- lib/media/media_source_engine.js | 8 ++- test/media/media_source_engine_integration.js | 64 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 87623e69e..fac7ffdb0 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -1042,7 +1042,13 @@ shaka.media.MediaSourceEngine = class { } else { // All other schemes are dispatched as a general 'emsg' event. - const endTime = startTime + (eventDuration / timescale); + let endTime = startTime; + // See: https://aomediacodec.github.io/id3-emsg/ + // ID3 EMSG events do not carry a duration + if (schemeId !== 'https://aomedia.org/emsg/ID3' || + eventDuration !== 0xFFFFFFFF) { + endTime += eventDuration / timescale; + } /** @type {shaka.extern.EmsgInfo} */ const emsg = { startTime: startTime, diff --git a/test/media/media_source_engine_integration.js b/test/media/media_source_engine_integration.js index c8634bcf6..789bb2c65 100644 --- a/test/media/media_source_engine_integration.js +++ b/test/media/media_source_engine_integration.js @@ -801,6 +801,35 @@ describe('MediaSourceEngine', () => { '53 68 61 6b 61 33 44 49 03 00 40 00 00 00 1b' ).replace(/\s/g, '')); + const emsgSegmentV0ID3WithoutDuration = Uint8ArrayUtils.fromHex(( + // 105 bytes emsg box v0, flags 0 + '00 00 00 69 65 6d 73 67 00 00 00 00' + + + // scheme id uri (13 bytes) 'https://aomedia.org/emsg/ID3' + '68 74 74 70 73 3a 2f 2f 61 6f 6d 65 64 69 61 2e' + + '6f 72 67 2f 65 6d 73 67 2f 49 44 33 00' + + + // value (1 byte) '' + '00' + + + // timescale (4 bytes) 1 + '00 00 00 01' + + + // presentation time delta (4 bytes) 8 + '00 00 00 08' + + + // event duration (4 bytes) 255 + 'ff ff ff ff' + + + // id (4 bytes) 51 + '00 00 00 33' + + + // message data (47 bytes) + '49 44 33 03 00 40 00 00 00 1b 00 00 00 06 00 00' + + '00 00 00 02 54 58 58 58 00 00 00 07 e0 00 03 00' + + '53 68 61 6b 61 33 44 49 03 00 40 00 00 00 1b' + ).replace(/\s/g, '')); + const id3SchemeUri = 'https://aomedia.org/emsg/ID3'; const emsgObj = { @@ -839,6 +868,18 @@ describe('MediaSourceEngine', () => { messageData: new Uint8Array([0x74, 0x65, 0x73, 0x74]), }; + const emsgObjSegmentV0ID3WithoutDuration = { + startTime: 8, + endTime: 8, + schemeIdUri: 'https://aomedia.org/emsg/ID3', + value: '', + timescale: 1, + presentationTimeDelta: 8, + eventDuration: 4294967295, + id: 51, + messageData: jasmine.any(Uint8Array), + }; + const initSegmentReference = new shaka.media.InitSegmentReference( /* uris= */ () => [], /* startByte= */ 0, @@ -989,6 +1030,29 @@ describe('MediaSourceEngine', () => { expect(onMetadata).toHaveBeenCalled(); }); + // eslint-disable-next-line @stylistic/max-len + it('triggers both emsg event and metadata event for ID3 without duration', () => { + const videoStream = + shaka.test.StreamingEngineUtil.createMockVideoStream(1); + videoStream.emsgSchemeIdUris = [id3SchemeUri]; + + onEvent.and.callFake((emsgEvent) => { + expect(emsgEvent.type).toBe('emsg'); + }); + + mediaSourceEngine.getTimestampAndDispatchMetadata( + ContentType.VIDEO, + emsgSegmentV0ID3WithoutDuration, + reference, + videoStream, + /* mimeType= */ 'video/mp4'); + + expect(onEmsg).toHaveBeenCalledTimes(1); + const emsgInfo = onEmsg.calls.argsFor(0)[0]; + expect(emsgInfo).toEqual(emsgObjSegmentV0ID3WithoutDuration); + expect(onMetadata).toHaveBeenCalled(); + }); + it('event start matches presentation time', () => { const videoStream = shaka.test.StreamingEngineUtil.createMockVideoStream(1);