From 6cae8ab8a0e2ef79d735c6fc4212f41a44ee5c40 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 9 Apr 2025 09:11:34 -0700 Subject: [PATCH] Support X-SNAP with HLS interstitials PiperOrigin-RevId: 745614349 --- .../hls/HlsInterstitialsAdsLoader.java | 109 ++- .../hls/HlsInterstitialsAdsLoaderTest.java | 835 ++++++++++++++++-- 2 files changed, 859 insertions(+), 85 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java index 524c26b936..a75f48d6c0 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java @@ -32,7 +32,11 @@ import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.common.util.Util.usToMs; import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_POST; import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.CUE_TRIGGER_PRE; +import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.SNAP_TYPE_IN; +import static androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Interstitial.SNAP_TYPE_OUT; +import static java.lang.Math.abs; import static java.lang.Math.max; +import static java.lang.Math.min; import android.content.Context; import android.net.Uri; @@ -633,7 +637,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader { window.positionInFirstPeriodUs, checkNotNull(insertedInterstitialIds.get(adsId))) : mapInterstitialsForVod( - window.mediaItem, + window, mediaPlaylist, adPlaybackState, checkNotNull(insertedInterstitialIds.get(adsId))); @@ -913,13 +917,15 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader { ArrayList interstitials = new ArrayList<>(mediaPlaylist.interstitials); for (int i = 0; i < interstitials.size(); i++) { Interstitial interstitial = interstitials.get(i); - long positionInPlaylistWindowUs = - interstitial.cue.contains(CUE_TRIGGER_PRE) - ? 0L - : (interstitial.startDateUnixUs - mediaPlaylist.startTimeUs); if (insertedInterstitialIds.contains(interstitial.id) - || interstitial.cue.contains(CUE_TRIGGER_POST) - || positionInPlaylistWindowUs < 0) { + || interstitial.cue.contains(CUE_TRIGGER_POST)) { + continue; + } + long positionInPlaylistWindowUs = + resolveInterstitialStartTimeUs(interstitial, mediaPlaylist) - mediaPlaylist.startTimeUs; + if (positionInPlaylistWindowUs < 0 || mediaPlaylist.durationUs < positionInPlaylistWindowUs) { + // Ignore when behind the window including C.TIME_UNSET and C.TIME_END_OF_SOURCE. + // When not yet in the window we wait until the window advances. continue; } long timeUs = windowPositionInPeriodUs + positionInPlaylistWindowUs; @@ -935,18 +941,18 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader { isNewAdGroup = false; break; } else if (adGroup.timeUs < timeUs) { - // Insert at index after group before interstitial. + // Insert at index after group behind interstitial. insertionIndex = adGroupIndex + 1; break; } - // Interstitial is before the ad group. Possible insertion index. + // Interstitial is behind the ad group. Possible insertion index. insertionIndex = adGroupIndex; } if (isNewAdGroup) { if (insertionIndex < getLowestValidAdGroupInsertionIndex(adPlaybackState)) { Log.w( TAG, - "Skipping insertion of interstitial attempted to be inserted before an already" + "Skipping insertion of interstitial attempted to be inserted behind an already" + " initialized ad group."); continue; } @@ -965,36 +971,49 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader { } private AdPlaybackState mapInterstitialsForVod( - MediaItem mediaItem, + Window window, HlsMediaPlaylist mediaPlaylist, AdPlaybackState adPlaybackState, Set insertedInterstitialIds) { - checkArgument(adPlaybackState.adGroupCount == 0); + checkArgument(adPlaybackState.adGroupCount == adPlaybackState.removedAdGroupCount); ImmutableList interstitials = mediaPlaylist.interstitials; + long clippedWindowStartTimeUs = mediaPlaylist.startTimeUs + window.positionInFirstPeriodUs; + long clippedWindowEndTimeUs = clippedWindowStartTimeUs + window.durationUs; for (int i = 0; i < interstitials.size(); i++) { Interstitial interstitial = interstitials.get(i); - long timeUs; - if (interstitial.cue.contains(CUE_TRIGGER_PRE)) { - timeUs = 0L; - } else if (interstitial.cue.contains(CUE_TRIGGER_POST)) { - timeUs = C.TIME_END_OF_SOURCE; - } else { - timeUs = interstitial.startDateUnixUs - mediaPlaylist.startTimeUs; + long interstitialStartTimeUs = resolveInterstitialStartTimeUs(interstitial, mediaPlaylist); + if (interstitialStartTimeUs < clippedWindowStartTimeUs + && interstitial.cue.contains(CUE_TRIGGER_PRE)) { + // Declared pre roll: move to the start of the clipped window. + interstitialStartTimeUs = clippedWindowStartTimeUs; + } else if (interstitialStartTimeUs > clippedWindowEndTimeUs + && interstitial.cue.contains(CUE_TRIGGER_POST)) { + // Declared post roll: move to the end of the clipped window. + interstitialStartTimeUs = clippedWindowEndTimeUs; + } else if (interstitialStartTimeUs < clippedWindowStartTimeUs + || clippedWindowEndTimeUs < interstitialStartTimeUs) { + // Ignore interstitials before or after the window that are not explicit pre or post rolls. + continue; } + long timeUs = + clippedWindowEndTimeUs == interstitialStartTimeUs + ? C.TIME_END_OF_SOURCE + : interstitialStartTimeUs - mediaPlaylist.startTimeUs; int adGroupIndex = adPlaybackState.getAdGroupIndexForPositionUs(timeUs, mediaPlaylist.durationUs); if (adGroupIndex == C.INDEX_UNSET) { // There is no ad group before or at the interstitials position. - adGroupIndex = 0; - adPlaybackState = adPlaybackState.withNewAdGroup(/* adGroupIndex= */ 0, timeUs); + adGroupIndex = adPlaybackState.removedAdGroupCount; + adPlaybackState = + adPlaybackState.withNewAdGroup(adPlaybackState.removedAdGroupCount, timeUs); } else if (adPlaybackState.getAdGroup(adGroupIndex).timeUs != timeUs) { - // There is an ad group before the interstitials. Insert after that index. + // There is an ad group before the interstitial. Insert after that index. adGroupIndex++; adPlaybackState = adPlaybackState.withNewAdGroup(adGroupIndex, timeUs); } adPlaybackState = insertOrUpdateInterstitialInAdGroup( - mediaItem, + window.mediaItem, interstitial, adPlaybackState, adGroupIndex, @@ -1021,7 +1040,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader { adIndexInAdGroup = max(adGroup.count, 0); // Append duration of new interstitial into existing ad durations. long interstitialDurationUs = - getInterstitialDurationUs(interstitial, /* defaultDurationUs= */ C.TIME_UNSET); + resolveInterstitialDurationUs(interstitial, /* defaultDurationUs= */ C.TIME_UNSET); long[] adDurations; if (adIndexInAdGroup == 0) { adDurations = new long[1]; @@ -1081,7 +1100,8 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader { return adPlaybackState.removedAdGroupCount; } - private static long getInterstitialDurationUs(Interstitial interstitial, long defaultDurationUs) { + private static long resolveInterstitialDurationUs( + Interstitial interstitial, long defaultDurationUs) { if (interstitial.playoutLimitUs != C.TIME_UNSET) { return interstitial.playoutLimitUs; } else if (interstitial.durationUs != C.TIME_UNSET) { @@ -1094,6 +1114,45 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader { return defaultDurationUs; } + private static long resolveInterstitialStartTimeUs( + Interstitial interstitial, HlsMediaPlaylist mediaPlaylist) { + if (interstitial.cue.contains(CUE_TRIGGER_PRE)) { + return mediaPlaylist.startTimeUs; + } else if (interstitial.cue.contains(CUE_TRIGGER_POST)) { + return mediaPlaylist.startTimeUs + mediaPlaylist.durationUs; + } else if (interstitial.snapTypes.contains(SNAP_TYPE_OUT)) { + return getClosestSegmentBoundaryUs(interstitial.startDateUnixUs, mediaPlaylist); + } else if (interstitial.snapTypes.contains(SNAP_TYPE_IN)) { + long resumeOffsetUs = + interstitial.resumeOffsetUs != C.TIME_UNSET + ? interstitial.resumeOffsetUs + : resolveInterstitialDurationUs(interstitial, /* defaultDurationUs= */ 0L); + return getClosestSegmentBoundaryUs( + interstitial.startDateUnixUs + resumeOffsetUs, mediaPlaylist) + - resumeOffsetUs; + } else { + return interstitial.startDateUnixUs; + } + } + + private static long getClosestSegmentBoundaryUs(long unixTimeUs, HlsMediaPlaylist mediaPlaylist) { + long positionInPlaylistUs = unixTimeUs - mediaPlaylist.startTimeUs; + if (positionInPlaylistUs <= 0 || mediaPlaylist.segments.isEmpty()) { + return mediaPlaylist.startTimeUs; + } else if (positionInPlaylistUs >= mediaPlaylist.durationUs) { + return mediaPlaylist.startTimeUs + mediaPlaylist.durationUs; + } + long segmentIndex = + min( + positionInPlaylistUs / mediaPlaylist.targetDurationUs, + mediaPlaylist.segments.size() - 1); + HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get((int) segmentIndex); + return positionInPlaylistUs - segment.relativeStartTimeUs + < abs(positionInPlaylistUs - (segment.relativeStartTimeUs + segment.durationUs)) + ? mediaPlaylist.startTimeUs + segment.relativeStartTimeUs + : mediaPlaylist.startTimeUs + segment.relativeStartTimeUs + segment.durationUs; + } + private class PlayerListener implements Player.Listener { private final Period period = new Period(); diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java index 6ae3ee8bb8..c390ef986c 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java @@ -265,6 +265,10 @@ public class HlsInterstitialsAdsLoaderTest { + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + "#EXTINF:6,\n" + "main1.0.ts\n" + + "#EXTINF:6,\n" + + "main2.0.ts\n" + + "#EXTINF:6,\n" + + "main3.0.ts\n" + "#EXT-X-ENDLIST" + "\n" + "#EXT-X-DATERANGE:" @@ -332,6 +336,108 @@ public class HlsInterstitialsAdsLoaderTest { assertThat(actual).isEqualTo(expected); } + @Test + public void handleContentTimelineChanged_clippedWindow_translatedToClippedWindow() + throws IOException { + AdPlaybackState actual = + callHandleContentTimelineChangedAndCaptureAdPlaybackState( + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6,\nmain1.0.ts\n" + + "#EXTINF:6,\nmain2.0.ts\n" + + "#EXTINF:6,\nmain3.0.ts\n" + + "#EXTINF:6,\nmain4.0.ts\n" // ends at 24_000_000 -> 21:56:04.000Z + + "#EXT-X-ENDLIST" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," // pre roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:44.000Z\"," + + "CUE=\"PRE\"," // Explicit pre roll. Aligned to clip start. + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad1-0\"," // ignored + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:42.999Z\"," // non-pre roll behind clipped window + + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad2-0\"," // ignored + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:39.999Z\"," // snaps to the playlist window start + + "X-SNAP=\"OUT\"" + + "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad3-0\"," // mid roll at 15_000_000 + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:55.000Z\"," + + "X-ASSET-URI=\"http://example.com/media-3-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad4-0\"," // post roll 0 + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:56:00.321Z\"," // exact match at end of clip + + "X-ASSET-URI=\"http://example.com/media-4-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad4-1\"," // ignored + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:56:00.322Z\"," // after end of clip + + "X-ASSET-URI=\"http://example.com/media-4-1.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad4-2\"," // post roll 1 + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2050-01-02T21:55:08.000Z\"," + + "CUE=\"POST\"," // explicit post roll + + "X-ASSET-URI=\"http://example.com/media-4-2.m3u8\"\n", + adsLoader, + /* windowIndex= */ 1, + 6_000_123L, // clipped to 6s after start of period + 20_321_000L); // clipped to 4s before end of period + assertThat(actual) + .isEqualTo( + new AdPlaybackState("adsId", 6_000_123L, 15_000_000L, C.TIME_END_OF_SOURCE) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdCount(/* adGroupIndex= */ 1, 1) + .withAdCount(/* adGroupIndex= */ 2, 2) + .withAdId(0, 0, "ad0-0") + .withAdId(1, 0, "ad3-0") + .withAdId(2, 0, "ad4-0") + .withAdId(2, 1, "ad4-2") + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-0-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-3-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 2, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-4-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 2, + /* adIndexInAdGroup= */ 1, + new MediaItem.Builder() + .setUri("http://example.com/media-4-2.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build())); + } + @Test public void handleContentTimelineChanged_3preRolls_mergedIntoSinglePreRollAdGroup() throws IOException { @@ -416,18 +522,21 @@ public class HlsInterstitialsAdsLoaderTest { + "ID=\"ad0-0\"," + "CLASS=\"com.apple.hls.interstitial\"," + "START-DATE=\"2020-01-02T21:55:44.000Z\"," + + "END-DATE=\"2020-01-02T21:55:46.000Z\"," // adds to resume offset + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + "\n" + "#EXT-X-DATERANGE:" + "ID=\"ad0-1\"," + "CLASS=\"com.apple.hls.interstitial\"," + "START-DATE=\"2020-01-02T21:55:44.000Z\"," + + "DURATION=1.1," // adds to resume offset + "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\"" + "\n" + "#EXT-X-DATERANGE:" + "ID=\"ad0-2\"," + "CLASS=\"com.apple.hls.interstitial\"," + "START-DATE=\"2020-01-02T21:55:44.000Z\"," + + "PLANNED-DURATION=1.2," // adds to resume offset + "X-ASSET-URI=\"http://example.com/media-0-2.m3u8\"" + "\n"; @@ -440,9 +549,9 @@ public class HlsInterstitialsAdsLoaderTest { /* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE)) .isEqualTo( new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 4_000_000L) - .withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET) + .withAdDurationsUs(/* adGroupIndex= */ 0, 2_000_000L, 1_100_000L, 1_200_000L) .withAdCount(/* adGroupIndex= */ 0, 3) - .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 0L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 4_300_000L) .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1") .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, "ad0-2") @@ -472,51 +581,49 @@ public class HlsInterstitialsAdsLoaderTest { @Test public void handleContentTimelineChanged_3postRolls_mergedIntoSinglePostRollAdGroup() throws IOException { - String playlistString = - "#EXTM3U\n" - + "#EXT-X-TARGETDURATION:6\n" - + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" - + "#EXTINF:6,\n" - + "main1.0.ts\n" - + "#EXT-X-ENDLIST" - + "\n" - + "#EXT-X-DATERANGE:" - + "ID=\"ad0-0\"," - + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:30.000Z\"," - + "END-DATE=\"2020-01-02T21:55:31.000Z\"," - + "CUE=\"POST\"," - + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" - + "\n" - + "#EXT-X-DATERANGE:" - + "ID=\"ad0-1\"," - + "CLASS=\"com.apple.hls.interstitial\"," - + "CUE=\"POST\"," - + "START-DATE=\"2020-01-02T21:55:40.000Z\"," - + "DURATION=1.1," - + "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\"" - + "\n" - + "#EXT-X-DATERANGE:" - + "ID=\"ad0-2\"," - + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:51.000Z\"," - + "CUE=\"POST\"," - + "PLANNED-DURATION=1.2," - + "X-ASSET-URI=\"http://example.com/media-0-2.m3u8\"" - + "\n"; - assertThat( callHandleContentTimelineChangedAndCaptureAdPlaybackState( - playlistString, + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6,\n" + + "main1.0.ts\n" + + "#EXT-X-ENDLIST" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:30.000Z\"," + + "CUE=\"POST\"," // cued as post roll + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-1\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:46.000Z\"," // exact match + + "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-2\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:46.001Z\"," // late but snaps to post roll. + + "X-SNAP=\"OUT\"" + + "X-ASSET-URI=\"http://example.com/media-0-2.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-3\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:46.001Z\"," // late and hence ignored. + + "X-ASSET-URI=\"http://example.com/media-0-3.m3u8\"" + + "\n", adsLoader, /* windowIndex= */ 0, /* windowPositionInPeriodUs= */ 0, /* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE)) .isEqualTo( new AdPlaybackState("adsId", /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE) - .withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L, 1_100_000L, 1_200_000L) + .withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET) .withAdCount(/* adGroupIndex= */ 0, 3) - .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 3_300_000L) .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1") .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, "ad0-2") @@ -909,25 +1016,608 @@ public class HlsInterstitialsAdsLoaderTest { } @Test - public void handleContentTimelineChanged_noDurationSet_durationTimeUnset() throws IOException { - String playlistString = - "#EXTM3U\n" - + "#EXT-X-TARGETDURATION:6\n" - + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" - + "#EXTINF:6,\n" - + "main1.0.ts\n" - + "#EXT-X-ENDLIST" - + "\n" - + "#EXT-X-DATERANGE:" - + "ID=\"ad0-0\"," - + "CLASS=\"com.apple.hls.interstitial\"," - + "START-DATE=\"2020-01-02T21:55:41.123Z\"," - + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" - + "\n"; - + public void handleContentTimelineChanged_snapOut_snapToClosestSegmentBoundaryOfStartPosition() + throws IOException { assertThat( callHandleContentTimelineChangedAndCaptureAdPlaybackState( - playlistString, + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6.111,\nmain1.ts\n" + + "#EXTINF:6.111,\nmain2.ts\n" + + "#EXTINF:6.111,\nmain3.ts\n" // end of window: 18_333_000 -> 21:55:58.333 + + "#EXT-X-ENDLIST" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," // post roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:58.000Z\"," // snap to 21:55:58.333 + + "X-SNAP=\"OUT\"," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad1-0\"," // mid roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:46.000Z\"," // snap to 21:55:46.111 + + "X-SNAP=\"OUT\"," + + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad3-0\"," // pre roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:40.123Z\"," // snap to 21:55:40.000 + + "X-SNAP=\"OUT\"," + + "X-ASSET-URI=\"http://example.com/media-3-0.m3u8\"" + + "\n", + adsLoader, + /* windowIndex= */ 0, + /* windowPositionInPeriodUs= */ 0, + /* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE)) + .isEqualTo( + new AdPlaybackState( + "adsId", /* adGroupTimesUs...= */ 0L, 6_111_000L, C.TIME_END_OF_SOURCE) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdCount(/* adGroupIndex= */ 1, 1) + .withAdCount(/* adGroupIndex= */ 2, 1) + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad3-0") + .withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0") + .withAdId(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, "ad0-0") + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-3-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-1-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 2, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-0-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build())); + } + + @Test + public void handleContentTimelineChanged_snapOutLive_snapToClosestSegmentBoundaryOfStartPosition() + throws IOException { + assertThat( + callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates( + adsLoader, + /* startAdsLoader= */ true, + /* windowOffsetInFirstPeriodUs= */ 2_000_123L, // window offset! + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," // mid roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:46.000\"," // snap to 6.111 + 2_000_123 + + "X-SNAP=\"OUT\"," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad1-0\"," // mid roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:52.999Z\"," // snap to 12.222 + 2_000_123 + + "X-SNAP=\"OUT\"," + + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad2-0\"," // mid roll at end of window + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2050-01-02T21:56:04.000Z\"," // snap to 18.333 + 2_000_123 + + "X-SNAP=\"OUT\"," + + "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\"" + + "\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" // 2s offset in period + + "#EXTINF:6.111,\nmain1.0.ts\n" + + "#EXTINF:6.111,\nmain2.0.ts\n" + + "#EXTINF:6.111,\nmain3.0.ts\n")) // window end time at 18.333 -> 21:55:58.333 + .containsExactly( + new AdPlaybackState( + "adsId", /* adGroupTimesUs...= */ 8_111_123L, 14_222_123L, 20_333_123L) + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdCount(/* adGroupIndex= */ 1, 1) + .withAdCount(/* adGroupIndex= */ 2, 1) + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") + .withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0") + .withAdId(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, "ad2-0") + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-0-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-1-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 2, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-2-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build())); + } + + @Test + public void handleContentTimelineChanged_snapIn_snapToClosestSegmentBoundaryOfResumptionPosition() + throws IOException { + assertThat( + callHandleContentTimelineChangedAndCaptureAdPlaybackState( + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6.111,\nmain1.ts\n" + + "#EXTINF:6.111,\nmain2.ts\n" // segment start at 6.111 + + "#EXTINF:6.111,\nmain3.ts\n" // end of window at 18.333 + + "#EXT-X-ENDLIST" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:39.900\"," // snap to 12.222 - 12.222 -> 0L + + "X-SNAP=\"IN\"," + + "DURATION=12.222," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad1-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:51.222\"," // aligned 12.222 - 3.222 -> 9_000L + + "X-SNAP=\"IN\"," + + "DURATION=3.222," + + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad2-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:54.678\"," // snap to end of window - 4.333 + + "X-SNAP=\"IN\"," + + "DURATION=4.333," + + "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\"" + + "\n", + adsLoader, + /* windowIndex= */ 0, + /* windowPositionInPeriodUs= */ 0, + /* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE)) + .isEqualTo( + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 9_000_000L, 14_000_000L) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdCount(/* adGroupIndex= */ 1, 1) + .withAdCount(/* adGroupIndex= */ 2, 1) + .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 12_222_000L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 1, 3_222_000L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 2, 4_333_000L) + .withAdDurationsUs(/* adGroupIndex= */ 0, 12_222_000L) + .withAdDurationsUs(/* adGroupIndex= */ 1, 3_222_000L) + .withAdDurationsUs(/* adGroupIndex= */ 2, 4_333_000L) + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") + .withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0") + .withAdId(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, "ad2-0") + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-0-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-1-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 2, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-2-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build())); + } + + @Test + public void + handleContentTimelineChanged_snapInLive_snapToClosestSegmentBoundaryOfResumptionPosition() + throws IOException { + assertThat( + callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates( + adsLoader, + /* startAdsLoader= */ true, + /* windowOffsetInFirstPeriodUs= */ 123L, + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:39.900\"," // snap to (12.222 - 12.222) -> 0L + + "X-SNAP=\"IN\"," + + "X-RESUME-OFFSET=12.222," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad1-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:51.222\"," // snap to (12.222 - 3.22) -> 9_000L + + "X-SNAP=\"IN\"," + + "X-RESUME-OFFSET=3.222," + + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad2-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:54.678\"," // snap to (end of window - 4.333) + + "X-SNAP=\"IN\"," + + "DURATION=4.333," + + "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\"" + + "\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6.111,\nmain1.ts\n" + + "#EXTINF:6.111,\nmain2.ts\n" // segment start at 6.111 + + "#EXTINF:6.111,\nmain3.ts\n")) // segment start at 12.222 + .containsExactly( + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 123L, 9_000_123L, 14_000_123L) + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdCount(/* adGroupIndex= */ 1, 1) + .withAdCount(/* adGroupIndex= */ 2, 1) + .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 12_222_000L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 1, 3_222_000L) + .withContentResumeOffsetUs(/* adGroupIndex= */ 2, 4_333_000L) + .withAdDurationsUs(/* adGroupIndex= */ 2, 4_333_000L) + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") + .withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0") + .withAdId(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, "ad2-0") + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-0-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-1-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 2, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-2-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build())); + } + + @Test + public void + handleContentTimelineChanged_snapInFarBeforeOrAfterWindow_snapToStartOfWindowAndPostRoll() + throws IOException { + assertThat( + callHandleContentTimelineChangedAndCaptureAdPlaybackState( + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6.111,\nmain1.0.ts\n" + + "#EXT-X-ENDLIST" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," // pre roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"1990-01-02T00:00:00.000\"," // snap to start of window. + + "DURATION=1.0," + + "X-RESUME-OFFSET=0.0," // with no offset SNAP_IN => SNAP_OUT + + "X-SNAP=\"IN\"," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad1-0\"," // ignored + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"1990-01-02T00:00:00.000\"," // snap end to start of window + + "DURATION=1.0," // translate start of ad back to 1s behind window + + "X-SNAP=\"IN\"," + + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad2-0\"," // post roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2050-01-02T21:55:00.900\"," // snap to end of window + + "X-SNAP=\"IN\"," + + "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\"" + + "\n", + adsLoader, + /* windowIndex= */ 0, + /* windowPositionInPeriodUs= */ 0, + /* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE)) + .isEqualTo( + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, C.TIME_END_OF_SOURCE) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdCount(/* adGroupIndex= */ 1, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L) + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") + .withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad2-0") + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-0-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-2-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build())); + } + + @Test + public void + handleContentTimelineChanged_snapInLiveFarBeforeOrAfterWindow_snapToStartAndEndOfWindow() + throws IOException { + assertThat( + callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates( + adsLoader, + /* startAdsLoader= */ true, + /* windowOffsetInFirstPeriodUs= */ 0, + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," // pre roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"1990-01-02T00:00:00.000\"," // snap to start of window + + "DURATION=1.0," + + "X-RESUME-OFFSET=0.0," // with no offset SNAP_IN => SNAP_OUT + + "X-SNAP=\"IN\"," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad1-0\"," // ignore + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"1990-01-02T00:00:00.000\"," // snap to start of window + + "DURATION=1.0," // translate start of ad back to 1s behind window + + "X-SNAP=\"IN\"," + + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad2-0\"," // mid roll at end of window (but not C.TIME_END_OF_SOURCE) + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2050-01-02T21:55:00.900\"," // snap end of window: 12_222_000 + + "X-SNAP=\"IN\"," // no duration or offset: SNAP_IN => SNAP_OUT + + "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\"" + + "\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6.111,\nmain1.0.ts\n" + + "#EXTINF:6.111,\nmain1.0.ts\n" // end of window: 12:222 - 21:55:52.222 + + "\n")) + .containsExactly( + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L, 12_222_000L) + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdCount(/* adGroupIndex= */ 1, 1) + .withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L) + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") + .withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad2-0") + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-0-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-2-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build())); + } + + @Test + public void + handleContentTimelineChanged_snapInOut_snapToSameSegmentBoundaryMergedIntoSameAdGroup() + throws IOException { + assertThat( + callHandleContentTimelineChangedAndCaptureAdPlaybackState( + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6.111,\nmain1.0.ts\n" + + "#EXTINF:6.111,\nmain2.0.ts\n" // segment start at 6.111 -> 21:55:46.111 + + "#EXTINF:6.111,\nmain3.0.ts\n" // segment start at 12.222 -> 21:55:52.222 + + "#EXT-X-ENDLIST" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," // mid roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:45.000\"," // snap tp 6_111_000 + + "X-SNAP=\"OUT\"," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-1\"," // mid roll + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:47.000\"," // snap to 6_111_000 + + "X-SNAP=\"IN\"," + + "DURATION=6.111," // ends at 21:55:53.111 -> 21:55:52.222 - 6.111 + + "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\"" + + "\n", + adsLoader, + /* windowIndex= */ 0, + /* windowPositionInPeriodUs= */ 0, + /* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE)) + .isEqualTo( + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 6_111_000L) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 6_111_000L) + .withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, 6_111_000L) + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1") + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-0-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 1, + new MediaItem.Builder() + .setUri("http://example.com/media-0-1.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build())); + } + + @Test + public void + handleContentTimelineChanged_snapInOutLive_snapToSameSegmentBoundaryMergedInSameAdGroup() + throws IOException { + assertThat( + callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates( + adsLoader, + /* startAdsLoader= */ true, + /* windowOffsetInFirstPeriodUs= */ 0L, + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:45.000\"," // -> 6_111_000L + + "X-SNAP=\"OUT\"," + + "DURATION=2.222," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-1\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:47.000\"," // -> 6_111_000L + + "X-SNAP=\"IN\"," + + "DURATION=6.111," + + "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\"" + + "\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6.111,\nmain1.0.ts\n" + + "#EXTINF:6.111,\nmain2.0.ts\n" // segment start at 6.111 -> 21:55:46.111 + + "#EXTINF:6.111,\nmain3.0.ts\n" // segment start at 12.222 -> 21:55:52.222 + + "\n")) + .containsExactly( + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 6_111_000L) + .withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false) + .withAdCount(/* adGroupIndex= */ 0, 2) + .withContentResumeOffsetUs(/* adGroupIndex= */ 0, 8_333_000L) + .withAdDurationsUs(/* adGroupIndex= */ 0, 2_222_000L, 6_111_000L) + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1") + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-0-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 1, + new MediaItem.Builder() + .setUri("http://example.com/media-0-1.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build())); + } + + @Test + public void handleContentTimelineChanged_snapOut_snapToExactTargetDurationBoundaryInWindow() + throws IOException { + assertThat( + callHandleContentTimelineChangedAndCaptureAdPlaybackState( + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6.111,\nmain0.ts\n" + + "#EXTINF:6.111,\nmain1.ts\n" + + "#EXTINF:6.111,\nmain2.ts\n" + + "#EXT-X-ENDLIST" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," + + "X-SNAP=\"OUT\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:46.000Z\"," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad1-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:58.000Z\"," + + "X-SNAP=\"OUT\"," + + "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"" + + "\n", + adsLoader, + /* windowIndex= */ 0, + /* windowPositionInPeriodUs= */ 0, + /* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE)) + .isEqualTo( + new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 6_111_000L, C.TIME_END_OF_SOURCE) + .withAdCount(/* adGroupIndex= */ 0, 1) + .withAdCount(/* adGroupIndex= */ 1, 1) + .withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0") + .withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0") + .withAvailableAdMediaItem( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-0-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build()) + .withAvailableAdMediaItem( + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + new MediaItem.Builder() + .setUri("http://example.com/media-1-0.m3u8") + .setMimeType(MimeTypes.APPLICATION_M3U8) + .build())); + } + + @Test + public void handleContentTimelineChanged_noDurationSet_durationTimeUnset() throws IOException { + assertThat( + callHandleContentTimelineChangedAndCaptureAdPlaybackState( + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:6\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z\n" + + "#EXTINF:6,\n" + + "main1.0.ts\n" + + "#EXT-X-ENDLIST" + + "\n" + + "#EXT-X-DATERANGE:" + + "ID=\"ad0-0\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"2020-01-02T21:55:41.123Z\"," + + "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\"" + + "\n", adsLoader, /* windowIndex= */ 0, /* windowPositionInPeriodUs= */ 0, @@ -1467,6 +2157,8 @@ public class HlsInterstitialsAdsLoaderTest { + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n" + "#EXTINF:9,\n" + "main0.ts\n" + + "#EXTINF:81,\n" + + "main0.ts\n" + "#EXT-X-ENDLIST" + "\n" + "#EXT-X-DATERANGE:" @@ -1597,6 +2289,8 @@ public class HlsInterstitialsAdsLoaderTest { + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n" + "#EXTINF:9,\n" + "main0.0.ts\n" + + "#EXTINF:16.001,\n" + + "main0.0.ts\n" + "#EXT-X-ENDLIST" + "\n" + "#EXT-X-DATERANGE:" @@ -1663,6 +2357,8 @@ public class HlsInterstitialsAdsLoaderTest { + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n" + "#EXTINF:9,\n" + "main0.0.ts\n" + + "#EXTINF:81,\n" + + "main0.0.ts\n" + "#EXT-X-ENDLIST" + "\n" + "#EXT-X-DATERANGE:" @@ -1752,8 +2448,10 @@ public class HlsInterstitialsAdsLoaderTest { + "X-ASSET-LIST=\"http://example.com/assetlist-1-0.json\"" + "\n" + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n" - + "#EXTINF:9,\n" - + "main0.0.ts\n"; + + "#EXTINF:9,\nmain0.0.ts\n" + + "#EXTINF:9,\nmain1.0.ts\n" + + "#EXTINF:9,\nmain2.0.ts\n" + + "#EXTINF:3,\nmain3.0.ts\n"; when(mockPlayer.getContentPosition()).thenReturn(0L); contentWindowDefinition = contentWindowDefinition @@ -1767,7 +2465,7 @@ public class HlsInterstitialsAdsLoaderTest { callHandleContentTimelineChangedAndCaptureAdPlaybackState( playlistString, adsLoader, - /* windowIndex= */ 0, + /* windowIndex= */ 2, /* windowPositionInPeriodUs= */ 0, /* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE); @@ -1891,6 +2589,10 @@ public class HlsInterstitialsAdsLoaderTest { + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n" + "#EXTINF:9,\n" + "main0.0.ts\n" + + "#EXTINF:9,\n" + + "main0.0.ts\n" + + "#EXTINF:9,\n" + + "main0.0.ts\n" + "#EXT-X-ENDLIST" + "\n" + "#EXT-X-DATERANGE:" @@ -1935,6 +2637,8 @@ public class HlsInterstitialsAdsLoaderTest { + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n" + "#EXTINF:9,\n" + "main0.0.ts\n" + + "#EXTINF:81,\n" + + "main0.0.ts\n" + "#EXT-X-ENDLIST" + "\n" + "#EXT-X-DATERANGE:" @@ -2194,6 +2898,10 @@ public class HlsInterstitialsAdsLoaderTest { + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n" + "#EXTINF:9,\n" + "main0.0.ts\n" + + "#EXTINF:9,\n" + + "main1.0.ts\n" + + "#EXTINF:72,\n" + + "main2.0.ts\n" + "#EXT-X-ENDLIST" + "\n" + "#EXT-X-DATERANGE:" @@ -2306,6 +3014,10 @@ public class HlsInterstitialsAdsLoaderTest { + "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n" + "#EXTINF:9,\n" + "main0.0.ts\n" + + "#EXTINF:9,\n" + + "main0.0.ts\n" + + "#EXTINF:9,\n" + + "main0.0.ts\n" + "#EXT-X-ENDLIST" + "\n" + "#EXT-X-DATERANGE:" @@ -3061,11 +3773,16 @@ public class HlsInterstitialsAdsLoaderTest { initialWindows[windowIndex] = contentWindowDefinition .buildUpon() + .setPlaceholder(true) + .setDynamic(true) + .setDurationUs(C.TIME_UNSET) .setDurationUs(durationUs) .setWindowPositionInFirstPeriodUs(windowPositionInPeriodUs) .build(); when(mockPlayer.getCurrentTimeline()).thenReturn(new FakeTimeline(initialWindows)); when(mockPlayer.getCurrentMediaItem()).thenReturn(contentWindowDefinition.mediaItem); + when(mockPlayer.getCurrentMediaItemIndex()).thenReturn(windowIndex); + when(mockPlayer.getCurrentPeriodIndex()).thenReturn(windowIndex); // Set the player. adsLoader.setPlayer(mockPlayer); // Start the ad. @@ -3096,8 +3813,6 @@ public class HlsInterstitialsAdsLoaderTest { .setAdPlaybackStates(ImmutableList.of(adPlaybackState.getValue())) .build(); when(mockPlayer.getCurrentTimeline()).thenReturn(new FakeTimeline(windowsAfterTimelineChange)); - when(mockPlayer.getCurrentMediaItemIndex()).thenReturn(windowIndex); - when(mockPlayer.getCurrentPeriodIndex()).thenReturn(windowIndex); return adPlaybackState.getValue(); }