From c8ee777226212008e21e73e1da8a2569d0cf1849 Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Sun, 6 Oct 2024 20:20:21 -0700 Subject: [PATCH] Handle Interstitials with empty asset-lists (skip to resumption offset or abutting interstitial) --- src/controller/interstitials-controller.ts | 38 ++++----- .../controller/interstitials-controller.ts | 77 +++++++++++++++++++ 2 files changed, 94 insertions(+), 21 deletions(-) diff --git a/src/controller/interstitials-controller.ts b/src/controller/interstitials-controller.ts index 302af792e48..0f42c196884 100644 --- a/src/controller/interstitials-controller.ts +++ b/src/controller/interstitials-controller.ts @@ -1045,7 +1045,7 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`, }); } const assetListLength = interstitial.assetList.length; - if (assetListLength === 0) { + if (assetListLength === 0 && !interstitial.assetListResponse) { // Waiting at end of primary content segment // Expect setSchedulePosition to be called again once ASSET-LIST is loaded this.log(`Waiting for ASSET-LIST to complete loading ${interstitial}`); @@ -1064,29 +1064,25 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`, // Update schedule and asset list position now that it can start this.waitingItem = null; this.playingItem = scheduledItem; - // Start Interstitial Playback + + // If asset-list is empty or missing asset index, advance to next item const assetItem = interstitial.assetList[assetListIndex]; if (!assetItem) { - const error = new Error( - `ASSET-LIST index ${assetListIndex} out of bounds [0-${ - assetListLength - 1 - }] ${interstitial}`, - ); - const errorData: ErrorData = { - fatal: true, - type: ErrorTypes.OTHER_ERROR, - details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR, - error, - }; - this.handleAssetItemError( - errorData, - interstitial, - index, - assetListIndex, - error.message, - ); + const nextItem = scheduleItems[index + 1]; + const media = this.media; + if ( + nextItem && + media && + !this.isInterstitial(nextItem) && + media.currentTime < nextItem.start + ) { + media.currentTime = this.timelinePos = nextItem.start; + } + this.advanceAfterAssetEnded(interstitial, index, assetListIndex || 0); return; } + + // Start Interstitial Playback if (!player) { player = this.getAssetPlayer(assetItem.identifier); } @@ -1411,7 +1407,7 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`, interstitialEvents.forEach((event) => (event.appendInPlace = false)); } this.log( - `Interstitial events (${ + `INTERSTITIALS_UPDATED (${ interstitialEvents.length }): ${interstitialEvents} Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`, diff --git a/tests/unit/controller/interstitials-controller.ts b/tests/unit/controller/interstitials-controller.ts index 5a8fae0bca3..d511fe3b204 100644 --- a/tests/unit/controller/interstitials-controller.ts +++ b/tests/unit/controller/interstitials-controller.ts @@ -1157,5 +1157,82 @@ fileSequence5.mp4`; expect(insterstitials.bufferingIndex).to.equal(0, 'bufferingIndex'); expect(insterstitials.playingIndex).to.equal(0, 'playingIndex'); }); + + it('should handle empty asset-lists', function () { + const playlist = `#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:7 +#EXT-X-MEDIA-SEQUENCE:1 +#EXT-X-PROGRAM-DATE-TIME:2024-02-23T15:00:00.000Z +#EXT-X-DATERANGE:ID="start",CLASS="com.apple.hls.interstitial",START-DATE="2024-02-23T15:00:00.000Z",DURATION=5,X-ASSET-LIST="https://example.com/empty.m3u8",X-RESUME-OFFSET=5 +#EXT-X-MAP:URI="fileSequence0.mp4" +#EXTINF:5, +fileSequence1.mp4 +#EXTINF:5, +fileSequence2.mp4 +#EXTINF:5, +fileSequence3.mp4 +#EXT-X-ENDLIST`; + const media = new MockMediaElement(); + hls.attachMedia(media as unknown as HTMLMediaElement); + (hls as any).bufferController.media = media; + hls.trigger(Events.MEDIA_ATTACHED, { + media: media as unknown as HTMLMediaElement, + mediaSource: {} as any, + }); + + const details = setLoadedLevelDetails(playlist); + hls.trigger(Events.LEVEL_UPDATED, { + details, + level: 0, + }); + const insterstitials = interstitialsController.interstitialsManager; + if (!insterstitials) { + expect(insterstitials, 'interstitialsManager').to.be.an('object'); + return; + } + expect(insterstitials.events).is.an('array').which.has.lengthOf(1); + expect(insterstitials.schedule).is.an('array').which.has.lengthOf(2); + if (!insterstitials.events || !insterstitials.schedule) { + return; + } + const callsBeforeAttach = getTriggerCalls(); + expect(callsBeforeAttach).to.deep.equal( + [ + Events.MEDIA_ATTACHING, + Events.MEDIA_ATTACHED, + Events.LEVEL_UPDATED, + Events.INTERSTITIALS_UPDATED, + Events.ASSET_LIST_LOADING, + Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY, + Events.INTERSTITIAL_STARTED, + ], + `Actual events before asset-list: ${callsBeforeAttach.join(', ')}`, + ); + hls.trigger.resetHistory(); + expect(insterstitials.bufferingIndex).to.equal(0, 'bufferingIndex'); + expect(insterstitials.playingIndex).to.equal(0, 'playingIndex'); + + // Load empty asset-list + const interstitial = insterstitials.events[0]; + interstitial.assetListResponse = { ASSETS: [] }; + hls.trigger(Events.ASSET_LIST_LOADED, { + event: interstitial, + assetListResponse: interstitial.assetListResponse, + networkDetails: {}, + }); + const callsAfterAttach = getTriggerCalls(); + expect(callsAfterAttach).to.deep.equal( + [ + Events.ASSET_LIST_LOADED, + Events.INTERSTITIAL_ENDED, + Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY, + Events.INTERSTITIALS_PRIMARY_RESUMED, + ], + `Actual events after asset-list: ${callsAfterAttach.join(', ')}`, + ); + expect(insterstitials.bufferingIndex).to.equal(1, 'bufferingIndex'); + expect(insterstitials.playingIndex).to.equal(1, 'playingIndex'); + }); }); });