Support X-SNAP with HLS interstitials

PiperOrigin-RevId: 745614349
This commit is contained in:
bachinger 2025-04-09 09:11:34 -07:00 committed by Copybara-Service
parent c5b6489d5d
commit 6cae8ab8a0
2 changed files with 859 additions and 85 deletions

View File

@ -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<Interstitial> 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<String> insertedInterstitialIds) {
checkArgument(adPlaybackState.adGroupCount == 0);
checkArgument(adPlaybackState.adGroupCount == adPlaybackState.removedAdGroupCount);
ImmutableList<Interstitial> 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();

View File

@ -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();
}