From ef72aa033f5cb56b981d762010cf250bb5da404f Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Mon, 9 Sep 2024 17:10:56 -0700 Subject: [PATCH] Fix schedule `previousEvent` for last item when last Interstitial is outside the primary program time range Addresses comment https://github.com/video-dev/hls.js/pull/6591#issuecomment-2339057006 (cherry picked from commit ebe6c08e22a779c90cba7cd8f257841898197e11) --- src/controller/interstitials-schedule.ts | 2 +- .../controller/interstitials-controller.ts | 101 +++++++++++++++++- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/controller/interstitials-schedule.ts b/src/controller/interstitials-schedule.ts index bcc774d21e4..98aea72663d 100644 --- a/src/controller/interstitials-schedule.ts +++ b/src/controller/interstitials-schedule.ts @@ -454,7 +454,7 @@ export class InterstitialsSchedule { const playoutStart = playoutDuration; playoutDuration += segmentDuration; schedule.push({ - previousEvent: interstitialEvents[interstitialEvents.length - 1], + previousEvent: schedule[schedule.length - 1].event || null, nextEvent: null, start: primaryPosition, end: timelineStart + segmentDuration, diff --git a/tests/unit/controller/interstitials-controller.ts b/tests/unit/controller/interstitials-controller.ts index c979265de16..5a8fae0bca3 100644 --- a/tests/unit/controller/interstitials-controller.ts +++ b/tests/unit/controller/interstitials-controller.ts @@ -53,7 +53,7 @@ function expectItemToHaveProperties( const item = schedule[itemIndex]; Object.keys(expected).forEach((key) => { // Use deep equals on all properties except for InterstitialEvents ('event' and 'nextEvent') - if (key === 'event' || key === 'nextEvent') { + if (key === 'event' || key === 'nextEvent' || key === 'previousEvent') { expect(item, 'Schedule Index ' + itemIndex) .to.be.an('object') .which.has.property(key); @@ -62,7 +62,7 @@ function expectItemToHaveProperties( JSON.stringify( item, (key, value) => - key === 'nextEvent' || key === 'event' + key === 'nextEvent' || key === 'previousEvent' || key === 'event' ? `${key} <${value ? value.identifier : value}>` : value, 2, @@ -83,7 +83,9 @@ function expectItemToHaveProperties( JSON.stringify( item, (key, value) => - key === 'nextEvent' || key === 'event' + key === 'nextEvent' || + key === 'previousEvent' || + key === 'event' ? `${key} <${value ? value.identifier : value}>` : value, 2, @@ -188,6 +190,7 @@ fileSequence4.ts expect(interstitialEvent.snapOptions.in).to.equal(true); expectScheduleToInclude(schedule, [ { + previousEvent: null, nextEvent: { identifier: '0', }, @@ -341,6 +344,9 @@ fileSequence4.ts }, }, { + previousEvent: { + identifier: '1', + }, nextEvent: { identifier: '2', }, @@ -401,6 +407,9 @@ fileSequence4.ts }, }, { + previousEvent: { + identifier: '4', + }, nextEvent: null, start: 38, end: 40, @@ -521,6 +530,7 @@ fileSequence3.ts }); expectScheduleToInclude(schedule, [ { + previousEvent: null, nextEvent: { identifier: '1', }, @@ -551,6 +561,9 @@ fileSequence3.ts }, }, { + previousEvent: { + identifier: '1', + }, nextEvent: { identifier: '2', }, @@ -641,6 +654,9 @@ fileSequence3.ts }, }, { + previousEvent: { + identifier: '5', + }, nextEvent: null, start: 24, end: 30, @@ -713,6 +729,9 @@ fileSequence3.mp4 }, }, { + previousEvent: { + identifier: 'ad1', + }, nextEvent: { identifier: 'ad2', }, @@ -743,6 +762,9 @@ fileSequence3.mp4 }, }, { + previousEvent: { + identifier: 'ad2', + }, nextEvent: null, start: 10, end: 30, @@ -758,6 +780,78 @@ fileSequence3.mp4 ]); }); + it('should exclude date ranges that start after the end of the primary playlist from the schedule and schedule item references', function () { + const playlist = `#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:10 +#EXT-X-MEDIA-SEQUENCE:1 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-PROGRAM-DATE-TIME:2021-01-04T05:00:00.000Z +#EXT-X-DATERANGE:ID="ad1",CLASS="com.apple.hls.interstitial",START-DATE="2021-01-04T05:00:00.000Z",DURATION=15,X-ASSET-LIST="https://example.com/asset_list.json",X-RESUME-OFFSET=0 +#EXT-X-DATERANGE:ID="ad2",CLASS="com.apple.hls.interstitial",START-DATE="2021-01-04T05:00:50.000Z",DURATION=30,X-ASSET-LIST="https://example.com/asset_list.json",X-RESUME-OFFSET=0 +#EXT-X-MAP:URI="fileSequence0.mp4" +#EXTINF:10, +fileSequence1.mp4 +#EXTINF:10, +fileSequence2.mp4 +#EXTINF:10, +fileSequence3.mp4 +#EXT-X-ENDLIST`; + 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; + } + const schedule = insterstitials.schedule; + expect(insterstitials.events).is.an('array').which.has.lengthOf(2); + expect(schedule).is.an('array').which.has.lengthOf(2); + if (!insterstitials.events || !schedule) { + return; + } + expect(insterstitials.events[0].identifier).to.equal('ad1'); + expect(insterstitials.events[1].identifier).to.equal('ad2'); + expect(insterstitials.events[0]).to.equal(schedule[0].event); + + expectScheduleToInclude(schedule, [ + { + event: { + identifier: 'ad1', + }, + start: 0, + end: 0, + playout: { + start: 0, + end: 15, + }, + integrated: { + start: 0, + end: 0, + }, + }, + { + previousEvent: { + identifier: 'ad1', + }, + nextEvent: null, + start: 0, + end: 30, + playout: { + start: 15, + end: 45, + }, + integrated: { + start: 0, + end: 30, + }, + }, + ]); + }); + describe('should parse timeline style and content may vary', function () { it('default values', function () { const playlist = `#EXTM3U @@ -906,6 +1000,7 @@ fileSequence4.ts }); expectScheduleToInclude(schedule, [ { + previousEvent: null, nextEvent: { identifier: '0', },