Map live interstitials to ad playback state

After this change, updates of the live HLS playlists are  reflected
in the ad playback state. Interstitials are inserted into new
or existing ad groups according to the current ad playback state.

PiperOrigin-RevId: 735733207
This commit is contained in:
bachinger 2025-03-11 06:21:05 -07:00 committed by Copybara-Service
parent ecac78f630
commit 593c6fa1e8
2 changed files with 780 additions and 144 deletions

View File

@ -15,11 +15,14 @@
*/
package androidx.media3.exoplayer.hls;
import static androidx.media3.common.AdPlaybackState.AD_STATE_UNAVAILABLE;
import static androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
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 java.lang.Math.max;
import android.content.Context;
@ -48,6 +51,7 @@ import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.ads.AdsLoader;
import androidx.media3.exoplayer.source.ads.AdsMediaSource;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.util.ArrayList;
@ -283,6 +287,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
private final PlayerListener playerListener;
private final Map<Object, EventListener> activeEventListeners;
private final Map<Object, AdPlaybackState> activeAdPlaybackStates;
private final Map<Object, Set<String>> insertedInterstitialIds;
private final List<Listener> listeners;
private final Set<Object> unsupportedAdsIds;
@ -294,6 +299,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
playerListener = new PlayerListener();
activeEventListeners = new HashMap<>();
activeAdPlaybackStates = new HashMap<>();
insertedInterstitialIds = new HashMap<>();
listeners = new ArrayList<>();
unsupportedAdsIds = new HashSet<>();
}
@ -366,18 +372,17 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
}
activeEventListeners.put(adsId, eventListener);
MediaItem mediaItem = adsMediaSource.getMediaItem();
if (player != null && isSupportedMediaItem(mediaItem, player.getCurrentTimeline())) {
if (isHlsMediaItem(mediaItem)) {
// Mark with NONE. Update and notify later when timeline with interstitials arrives.
activeAdPlaybackStates.put(adsId, AdPlaybackState.NONE);
insertedInterstitialIds.put(adsId, new HashSet<>());
notifyListeners(listener -> listener.onStart(mediaItem, adsId, adViewProvider));
} else {
putAndNotifyAdPlaybackStateUpdate(adsId, new AdPlaybackState(adsId));
if (player != null) {
Log.w(TAG, "Unsupported media item. Playing without ads for adsId=" + adsId);
unsupportedAdsIds.add(adsId);
}
}
}
@Override
public void handleContentTimelineChanged(AdsMediaSource adsMediaSource, Timeline timeline) {
@ -387,6 +392,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
if (eventListener != null) {
unsupportedAdsIds.remove(adsId);
AdPlaybackState adPlaybackState = checkNotNull(activeAdPlaybackStates.remove(adsId));
insertedInterstitialIds.remove(adsId);
if (adPlaybackState.equals(AdPlaybackState.NONE)) {
// Play without ads after release to not interrupt playback.
eventListener.onAdPlaybackState(new AdPlaybackState(adsId));
@ -394,17 +400,35 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
}
return;
}
AdPlaybackState adPlaybackState = checkNotNull(activeAdPlaybackStates.get(adsId));
if (!adPlaybackState.equals(AdPlaybackState.NONE)) {
// VOD only. Updating the playback state is not supported yet.
if (!adPlaybackState.equals(AdPlaybackState.NONE)
&& !adPlaybackState.endsWithLivePostrollPlaceHolder()) {
// Multiple timeline updates for VOD not supported.
return;
}
if (adPlaybackState.equals(AdPlaybackState.NONE)) {
// Setup initial ad playback state for VOD or live.
adPlaybackState = new AdPlaybackState(adsId);
if (isLiveMediaItem(adsMediaSource.getMediaItem(), timeline)) {
adPlaybackState =
adPlaybackState.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false);
}
}
Window window = timeline.getWindow(0, new Window());
if (window.manifest instanceof HlsManifest) {
HlsMediaPlaylist mediaPlaylist = ((HlsManifest) window.manifest).mediaPlaylist;
adPlaybackState =
mapHlsInterstitialsToAdPlaybackState(
((HlsManifest) window.manifest).mediaPlaylist, adPlaybackState);
window.isLive()
? mapInterstitialsForLive(
mediaPlaylist,
adPlaybackState,
window.positionInFirstPeriodUs,
checkNotNull(insertedInterstitialIds.get(adsId)))
: mapInterstitialsForVod(
mediaPlaylist, adPlaybackState, checkNotNull(insertedInterstitialIds.get(adsId)));
}
putAndNotifyAdPlaybackStateUpdate(adsId, adPlaybackState);
if (!unsupportedAdsIds.contains(adsId)) {
@ -464,6 +488,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
adsMediaSource.getAdsId(),
checkNotNull(adPlaybackState)));
}
insertedInterstitialIds.remove(adsId);
unsupportedAdsIds.remove(adsId);
}
@ -488,6 +513,7 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
eventListener.onAdPlaybackState(adPlaybackState);
} else {
activeAdPlaybackStates.remove(adsId);
insertedInterstitialIds.remove(adsId);
}
}
}
@ -498,10 +524,6 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
}
}
private static boolean isSupportedMediaItem(MediaItem mediaItem, Timeline timeline) {
return isHlsMediaItem(mediaItem) && !isLiveMediaItem(mediaItem, timeline);
}
private static boolean isLiveMediaItem(MediaItem mediaItem, Timeline timeline) {
int windowIndex = timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false);
Window window = new Window();
@ -523,68 +545,161 @@ public final class HlsInterstitialsAdsLoader implements AdsLoader {
|| Util.inferContentType(localConfiguration.uri) == C.CONTENT_TYPE_HLS;
}
private static AdPlaybackState mapHlsInterstitialsToAdPlaybackState(
HlsMediaPlaylist hlsMediaPlaylist, AdPlaybackState adPlaybackState) {
for (int i = 0; i < hlsMediaPlaylist.interstitials.size(); i++) {
Interstitial interstitial = hlsMediaPlaylist.interstitials.get(i);
private static AdPlaybackState mapInterstitialsForLive(
HlsMediaPlaylist mediaPlaylist,
AdPlaybackState adPlaybackState,
long windowPositionInPeriodUs,
Set<String> insertedInterstitialIds) {
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 (interstitial.assetUri == null
|| insertedInterstitialIds.contains(interstitial.id)
|| interstitial.cue.contains(CUE_TRIGGER_POST)
|| positionInPlaylistWindowUs < 0) {
continue;
}
long timeUs = windowPositionInPeriodUs + positionInPlaylistWindowUs;
int insertionIndex = adPlaybackState.adGroupCount - 1;
boolean isNewAdGroup = true;
for (int adGroupIndex = adPlaybackState.adGroupCount - 2; // skip live placeholder
adGroupIndex >= adPlaybackState.removedAdGroupCount;
adGroupIndex--) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
if (adGroup.timeUs == timeUs) {
// Insert interstitials into or update in existing group.
insertionIndex = adGroupIndex;
isNewAdGroup = false;
break;
} else if (adGroup.timeUs < timeUs) {
// Insert at index after group before interstitial.
insertionIndex = adGroupIndex + 1;
break;
}
// Interstitial is before 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"
+ " initialized ad group.");
continue;
}
adPlaybackState = adPlaybackState.withNewAdGroup(insertionIndex, timeUs);
}
adPlaybackState =
insertOrUpdateInterstitialInAdGroup(
interstitial, /* adGroupIndex= */ insertionIndex, adPlaybackState);
insertedInterstitialIds.add(interstitial.id);
}
return adPlaybackState;
}
private static AdPlaybackState mapInterstitialsForVod(
HlsMediaPlaylist mediaPlaylist,
AdPlaybackState adPlaybackState,
Set<String> insertedInterstitialIds) {
checkArgument(adPlaybackState.adGroupCount == 0);
ImmutableList<Interstitial> interstitials = mediaPlaylist.interstitials;
for (int i = 0; i < interstitials.size(); i++) {
Interstitial interstitial = interstitials.get(i);
if (interstitial.assetUri == null) {
Log.w(TAG, "Ignoring interstitials with X-ASSET-LIST. Not yet supported.");
continue;
}
long positionUs;
if (interstitial.cue.contains(Interstitial.CUE_TRIGGER_PRE)) {
positionUs = 0;
} else if (interstitial.cue.contains(Interstitial.CUE_TRIGGER_POST)) {
positionUs = C.TIME_END_OF_SOURCE;
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 {
positionUs = interstitial.startDateUnixUs - hlsMediaPlaylist.startTimeUs;
timeUs = interstitial.startDateUnixUs - mediaPlaylist.startTimeUs;
}
// Check whether and at which index to insert an ad group for the interstitial start time.
int adGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(
positionUs, /* periodDurationUs= */ hlsMediaPlaylist.durationUs);
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(0, positionUs);
} else if (adPlaybackState.getAdGroup(adGroupIndex).timeUs != positionUs) {
adPlaybackState = adPlaybackState.withNewAdGroup(/* adGroupIndex= */ 0, timeUs);
} else if (adPlaybackState.getAdGroup(adGroupIndex).timeUs != timeUs) {
// There is an ad group before the interstitials. Insert after that index.
adGroupIndex++;
adPlaybackState = adPlaybackState.withNewAdGroup(adGroupIndex, positionUs);
adPlaybackState = adPlaybackState.withNewAdGroup(adGroupIndex, timeUs);
}
adPlaybackState =
insertOrUpdateInterstitialInAdGroup(interstitial, adGroupIndex, adPlaybackState);
insertedInterstitialIds.add(interstitial.id);
}
return adPlaybackState;
}
int adIndexInAdGroup = max(adPlaybackState.getAdGroup(adGroupIndex).count, 0);
private static AdPlaybackState insertOrUpdateInterstitialInAdGroup(
Interstitial interstitial, int adGroupIndex, AdPlaybackState adPlaybackState) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex);
int adIndexInAdGroup = adGroup.getIndexOfAdId(interstitial.id);
if (adIndexInAdGroup != C.INDEX_UNSET) {
// Interstitial already inserted. Updating not yet supported.
return adPlaybackState;
}
// Insert duration of new interstitial into existing ad durations.
// Append to the end of the group.
adIndexInAdGroup = max(adGroup.count, 0);
// Append duration of new interstitial into existing ad durations.
long interstitialDurationUs =
getInterstitialDurationUs(interstitial, /* defaultDurationUs= */ C.TIME_UNSET);
long[] adDurations;
if (adIndexInAdGroup == 0) {
adDurations = new long[1];
} else {
long[] previousDurations = adPlaybackState.getAdGroup(adGroupIndex).durationsUs;
long[] previousDurations = adGroup.durationsUs;
adDurations = new long[previousDurations.length + 1];
System.arraycopy(previousDurations, 0, adDurations, 0, previousDurations.length);
}
adDurations[adDurations.length - 1] = interstitialDurationUs;
long resumeOffsetIncrementUs =
interstitial.resumeOffsetUs != C.TIME_UNSET
? interstitial.resumeOffsetUs
: (interstitialDurationUs != C.TIME_UNSET ? interstitialDurationUs : 0L);
long resumeOffsetUs =
adPlaybackState.getAdGroup(adGroupIndex).contentResumeOffsetUs + resumeOffsetIncrementUs;
long resumeOffsetUs = adGroup.contentResumeOffsetUs + resumeOffsetIncrementUs;
adPlaybackState =
adPlaybackState
.withAdCount(adGroupIndex, /* adCount= */ adIndexInAdGroup + 1)
.withAdCount(adGroupIndex, adIndexInAdGroup + 1)
.withAdId(adGroupIndex, adIndexInAdGroup, interstitial.id)
.withAdDurationsUs(adGroupIndex, adDurations)
.withContentResumeOffsetUs(adGroupIndex, resumeOffsetUs)
.withAvailableAdMediaItem(
adGroupIndex, adIndexInAdGroup, MediaItem.fromUri(interstitial.assetUri));
.withContentResumeOffsetUs(adGroupIndex, resumeOffsetUs);
if (interstitial.assetUri != null) {
adPlaybackState =
adPlaybackState.withAvailableAdMediaItem(
adGroupIndex,
adIndexInAdGroup,
new MediaItem.Builder()
.setUri(interstitial.assetUri)
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build());
}
return adPlaybackState;
}
private static int getLowestValidAdGroupInsertionIndex(AdPlaybackState adPlaybackState) {
for (int adGroupIndex = adPlaybackState.adGroupCount - 1;
adGroupIndex >= adPlaybackState.removedAdGroupCount;
adGroupIndex--) {
for (@AdPlaybackState.AdState int state : adPlaybackState.getAdGroup(adGroupIndex).states) {
if (state != AD_STATE_UNAVAILABLE) {
return adGroupIndex + 1;
}
}
}
// All ad groups unavailable.
return adPlaybackState.removedAdGroupCount;
}
private static long getInterstitialDurationUs(Interstitial interstitial, long defaultDurationUs) {
if (interstitial.playoutLimitUs != C.TIME_UNSET) {
return interstitial.playoutLimitUs;

View File

@ -21,6 +21,7 @@ import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
@ -36,6 +37,7 @@ import androidx.media3.common.AdViewProvider;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Player;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSpec;
@ -45,12 +47,14 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.ads.AdsLoader;
import androidx.media3.exoplayer.source.ads.AdsMediaSource;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@ -76,8 +80,8 @@ public class HlsInterstitialsAdsLoaderTest {
private MediaItem contentMediaItem;
private DataSpec adTagDataSpec;
private AdsMediaSource adsMediaSource;
private FakeTimeline.TimelineWindowDefinition contentWindowDefinition;
private FakeTimeline.TimelineWindowDefinition adsMediaSourceWindowDefinition;
private TimelineWindowDefinition contentWindowDefinition;
private TimelineWindowDefinition adsMediaSourceWindowDefinition;
@Before
public void setUp() {
@ -101,13 +105,13 @@ public class HlsInterstitialsAdsLoaderTest {
.createMediaSource(contentMediaItem);
// The content timeline with empty ad playback state.
contentWindowDefinition =
new FakeTimeline.TimelineWindowDefinition.Builder()
new TimelineWindowDefinition.Builder()
.setDurationUs(90_000_000L)
.setMediaItem(contentMediaItem)
.build();
// The ads timeline with a minimal ad playback state with the ads ID.
adsMediaSourceWindowDefinition =
new FakeTimeline.TimelineWindowDefinition.Builder()
new TimelineWindowDefinition.Builder()
.setDurationUs(90_000_000L)
.setMediaItem(contentMediaItem)
.setAdPlaybackStates(ImmutableList.of(new AdPlaybackState("adsId")))
@ -152,7 +156,7 @@ public class HlsInterstitialsAdsLoaderTest {
when(mockPlayer.getCurrentTimeline())
.thenReturn(
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition.Builder()
new TimelineWindowDefinition.Builder()
.setDynamic(true)
.setDurationUs(C.TIME_UNSET)
.setMediaItem(mp4MediaItem)
@ -164,24 +168,6 @@ public class HlsInterstitialsAdsLoaderTest {
verify(mockEventListener).onAdPlaybackState(new AdPlaybackState("adsId"));
}
@Test
public void start_liveWindow_emptyAdPlaybackState() {
when(mockPlayer.getCurrentTimeline())
.thenReturn(
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition.Builder()
.setDynamic(true)
.setLive(true)
.setDurationUs(C.TIME_UNSET)
.setMediaItem(contentMediaItem)
.build()));
adsLoader.setPlayer(mockPlayer);
adsLoader.start(adsMediaSource, adTagDataSpec, "adsId", mockAdViewProvider, mockEventListener);
verify(mockEventListener).onAdPlaybackState(new AdPlaybackState("adsId"));
}
@Test
public void start_twiceWithIdenticalAdsId_throwIllegalStateException() {
when(mockPlayer.getCurrentTimeline()).thenReturn(new FakeTimeline(contentWindowDefinition));
@ -220,7 +206,7 @@ public class HlsInterstitialsAdsLoaderTest {
when(mockPlayer.getCurrentTimeline())
.thenReturn(
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition.Builder()
new TimelineWindowDefinition.Builder()
.setDynamic(true)
.setDurationUs(C.TIME_UNSET)
.setMediaItem(mp4MediaItem)
@ -249,27 +235,28 @@ public class HlsInterstitialsAdsLoaderTest {
+ "#EXT-X-ENDLIST"
+ "\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad0\","
+ "ID=\"ad0-0\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:55:44.000Z\","
+ "CUE=\"PRE\","
+ "X-ASSET-URI=\"http://example.com/media-0.m3u8\""
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
+ "\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad1\","
+ "ID=\"ad1-0\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:55:55.000Z\","
+ "X-ASSET-URI=\"http://example.com/media-1.m3u8\""
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\""
+ "\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad2\","
+ "ID=\"ad2-0\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:55:44.000Z\","
+ "CUE=\"POST\","
+ "X-ASSET-URI=\"http://example.com/media-2.m3u8\"\n";
+ "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\"\n";
assertThat(callHandleContentTimelineChangedAndCaptureAdPlaybackState(playlistString, adsLoader))
.isEqualTo(
AdPlaybackState actual =
callHandleContentTimelineChangedAndCaptureAdPlaybackState(playlistString, adsLoader);
AdPlaybackState expected =
new AdPlaybackState("adsId", 0L, 15_000_000L, C.TIME_END_OF_SOURCE)
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET)
.withAdDurationsUs(/* adGroupIndex= */ 1, C.TIME_UNSET)
@ -280,18 +267,31 @@ public class HlsInterstitialsAdsLoaderTest {
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 0L)
.withContentResumeOffsetUs(/* adGroupIndex= */ 1, 0L)
.withContentResumeOffsetUs(/* adGroupIndex= */ 2, 0L)
.withAdId(0, 0, "ad0-0")
.withAdId(1, 0, "ad1-0")
.withAdId(2, 0, "ad2-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 1,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-1.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-1-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 2,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-2.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-2-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build());
assertThat(actual).isEqualTo(expected);
}
@Test
@ -331,18 +331,30 @@ public class HlsInterstitialsAdsLoaderTest {
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET)
.withAdCount(/* adGroupIndex= */ 0, 3)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 0L)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1")
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, "ad0-2")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 1,
MediaItem.fromUri("http://example.com/media-0-1.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-1.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 2,
MediaItem.fromUri("http://example.com/media-0-2.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-0-2.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()));
}
@Test
@ -381,18 +393,30 @@ public class HlsInterstitialsAdsLoaderTest {
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET)
.withAdCount(/* adGroupIndex= */ 0, 3)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 0L)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1")
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, "ad0-2")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 1,
MediaItem.fromUri("http://example.com/media-0-1.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-1.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 2,
MediaItem.fromUri("http://example.com/media-0-2.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-0-2.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()));
}
@Test
@ -437,18 +461,30 @@ public class HlsInterstitialsAdsLoaderTest {
.withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L, 1_100_000L, 1_200_000L)
.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")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 1,
MediaItem.fromUri("http://example.com/media-0-1.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-1.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 2,
MediaItem.fromUri("http://example.com/media-0-2.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-0-2.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()));
}
@Test
@ -463,7 +499,7 @@ public class HlsInterstitialsAdsLoaderTest {
+ "#EXT-X-ENDLIST"
+ "\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad0-2\","
+ "ID=\"ad2-0\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:55:40.500Z\","
+ "CUE=\"POST\","
@ -471,7 +507,7 @@ public class HlsInterstitialsAdsLoaderTest {
+ "X-ASSET-URI=\"http://example.com/media-2-0.m3u8\""
+ "\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad0-1\","
+ "ID=\"ad1-0\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:55:42.000Z\","
+ "DURATION=2.0,"
@ -498,18 +534,30 @@ public class HlsInterstitialsAdsLoaderTest {
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 1_000_000L)
.withContentResumeOffsetUs(/* adGroupIndex= */ 1, 2_000_000L)
.withContentResumeOffsetUs(/* adGroupIndex= */ 2, 3_000_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,
MediaItem.fromUri("http://example.com/media-0-0.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 1,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-1-0.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-1-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 2,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-2-0.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-2-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()));
}
@Test
@ -533,7 +581,7 @@ public class HlsInterstitialsAdsLoaderTest {
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
+ "\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad0-0\","
+ "ID=\"ad0-1\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:55:41.123Z\","
+ "DURATION=1.0,"
@ -547,14 +595,22 @@ public class HlsInterstitialsAdsLoaderTest {
.withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L, 1_000_000L)
.withAdCount(/* adGroupIndex= */ 0, 2)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 1_000_000L)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 1,
MediaItem.fromUri("http://example.com/media-0-1.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-0-1.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()));
}
@Test
@ -578,7 +634,7 @@ public class HlsInterstitialsAdsLoaderTest {
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
+ "\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad0-0\","
+ "ID=\"ad0-1\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:55:41.123Z\","
+ "CUE=\"PRE\","
@ -591,14 +647,22 @@ public class HlsInterstitialsAdsLoaderTest {
.withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L, C.TIME_UNSET)
.withAdCount(/* adGroupIndex= */ 0, 2)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 0L)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 1,
MediaItem.fromUri("http://example.com/media-0-1.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-0-1.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()));
}
@Test
@ -629,10 +693,14 @@ public class HlsInterstitialsAdsLoaderTest {
.withAdDurationsUs(/* adGroupIndex= */ 0, 4_000_000L)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 4_000_000L)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()));
}
@Test
@ -662,10 +730,14 @@ public class HlsInterstitialsAdsLoaderTest {
.withAdDurationsUs(/* adGroupIndex= */ 0, 3_456_000L)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 3_456_000L)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()));
}
@Test
@ -693,10 +765,14 @@ public class HlsInterstitialsAdsLoaderTest {
.withAdDurationsUs(/* adGroupIndex= */ 0, 1_123_000L)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 1_123_000L)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()));
}
@Test
@ -724,10 +800,14 @@ public class HlsInterstitialsAdsLoaderTest {
.withAdDurationsUs(/* adGroupIndex= */ 0, 2_234_000L)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 2_234_000L)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()));
}
@Test
@ -753,10 +833,391 @@ public class HlsInterstitialsAdsLoaderTest {
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 0L)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8")));
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()));
}
@Test
public void handleContentTimelineChanged_livePlaylistWithoutInterstitials_hasLivePlaceholder()
throws IOException {
assertThat(
callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates(
adsLoader,
/* startAdsLoader= */ true,
/* windowOffsetInFirstPeriodUs= */ 0L,
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:6\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
+ "#EXTINF:6,\nmain0.0.ts\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"
+ "\n",
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:6\n"
+ "#EXT-X-MEDIA-SEQUENCE:1\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:06.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"
+ "#EXTINF:6,\nmain5.0.ts\n"
+ "\n"))
.containsExactly(
new AdPlaybackState("adsId")
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false));
}
@Test
public void
handleContentTimelineChanged_threeLivePlaylistUpdatesUnplayed_correctAdPlaybackStateUpdates()
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:00:06.000Z\","
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
+ "\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
+ "#EXTINF:6,\nmain0.0.ts\n"
+ "#EXTINF:6,\nmain1.0.ts\n" // ad0-0 cue point: 21:00:06
+ "#EXTINF:6,\nmain2.0.ts\n"
+ "#EXTINF:6,\nmain3.0.ts\n"
+ "#EXTINF:6,\nmain4.0.ts\n"
+ "\n",
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:6\n"
+ "#EXT-X-MEDIA-SEQUENCE:1\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad0-0\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:00:06.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:00:18.000Z\","
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:06.000Z\n"
+ "#EXTINF:6,\nmain1.0.ts\n" // ad0-0 cue point: 21:00:06
+ "#EXTINF:6,\nmain2.0.ts\n"
+ "#EXTINF:6,\nmain3.0.ts\n" // ad1-0 cue point: 21:00:18
+ "#EXTINF:6,\nmain4.0.ts\n"
+ "#EXTINF:6,\nmain5.0.ts\n"
+ "\n",
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:6\n"
+ "#EXT-X-MEDIA-SEQUENCE:2\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad1-0\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:00:18.000Z\","
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"\n"
+ "\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad1-1\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:00:18.000Z\","
+ "X-ASSET-URI=\"http://example.com/media-1-1.m3u8\"\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:12.000Z\n"
+ "#EXTINF:6,\nmain2.0.ts\n"
+ "#EXTINF:6,\nmain3.0.ts\n" // ad1-0 cue point: 21:00:18
+ "#EXTINF:6,\nmain4.0.ts\n"
+ "#EXTINF:6,\nmain5.0.ts\n"
+ "#EXTINF:6,\nmain6.0.ts\n"
+ "\n"))
.containsExactly(
new AdPlaybackState("adsId", 6_000_000L)
.withAdResumePositionUs(0)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false),
new AdPlaybackState("adsId", 6_000_000L)
.withAdResumePositionUs(0)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withNewAdGroup(1, 18_000_000L)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)
.withAdId(1, 0, "ad1-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 1,
/* adIndexInAdGroup= */ 0,
new MediaItem.Builder()
.setUri("http://example.com/media-1-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false),
new AdPlaybackState("adsId", 6_000_000L)
.withAdResumePositionUs(0)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withNewAdGroup(1, 18_000_000L)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 2)
.withAdId(1, 0, "ad1-0")
.withAdId(1, 1, "ad1-1")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 1,
/* adIndexInAdGroup= */ 0,
new MediaItem.Builder()
.setUri("http://example.com/media-1-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 1,
/* adIndexInAdGroup= */ 1,
new MediaItem.Builder()
.setUri("http://example.com/media-1-1.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false))
.inOrder();
}
@Test
public void
handleContentTimelineChanged_livePlaylistUpdateNewAdAfterPlayedAd_correctAdPlaybackStateUpdates()
throws IOException {
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:00:06.000Z\","
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
+ "\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
+ "#EXTINF:6,\nmain0.0.ts\n"
+ "#EXTINF:6,\nmain1.0.ts\n" // ad0-0 cue point: 21:00:06
+ "#EXTINF:6,\nmain2.0.ts\n"
+ "#EXTINF:6,\nmain3.0.ts\n"
+ "#EXTINF:6,\nmain4.0.ts\n"
+ "\n");
reset(mockEventListener);
// Mark ad as played by a automatic discontinuity from the ad to the content.
ArgumentCaptor<Player.Listener> listener = ArgumentCaptor.forClass(Player.Listener.class);
verify(mockPlayer).addListener(listener.capture());
Object windowUid = new Object();
Object periodUid = new Object();
listener
.getValue()
.onPositionDiscontinuity(
new Player.PositionInfo(
windowUid,
/* mediaItemIndex= */ 0,
contentMediaItem,
periodUid,
/* periodIndex= */ 0,
/* positionMs= */ 10_000L,
/* contentPositionMs= */ 0L,
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0),
new Player.PositionInfo(
windowUid,
/* mediaItemIndex= */ 0,
contentMediaItem,
periodUid,
/* periodIndex= */ 0,
/* positionMs= */ 0L,
/* contentPositionMs= */ 0L,
/* adGroupIndex= */ C.INDEX_UNSET,
/* adIndexInAdGroup= */ C.INDEX_UNSET),
DISCONTINUITY_REASON_AUTO_TRANSITION);
verify(mockEventListener)
.onAdPlaybackState(
new AdPlaybackState("adsId", 6_000_000L)
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false)
.withAdResumePositionUs(0)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
reset(mockEventListener);
assertThat(
callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates(
adsLoader,
/* startAdsLoader= */ false,
/* windowOffsetInFirstPeriodUs= */ 6_000_000L,
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:6\n"
+ "#EXT-X-MEDIA-SEQUENCE:1\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad0-0\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:00:06.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:00:18.000Z\","
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\"\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:06.000Z\n"
+ "#EXTINF:6,\nmain1.0.ts\n" // ad0-0 cue point: 21:00:06
+ "#EXTINF:6,\nmain2.0.ts\n"
+ "#EXTINF:6,\nmain3.0.ts\n"
+ "#EXTINF:6,\nmain4.0.ts\n"
+ "#EXTINF:6,\nmain5.0.ts\n" // ad1-0 cue point: 21:00:30
+ "\n"))
.containsExactly(
new AdPlaybackState("adsId", 6_000_000L)
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false)
.withAdResumePositionUs(0)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withNewAdGroup(1, 18_000_000L)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)
.withAdId(1, 0, "ad1-0")
.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_attemptInsertionForLiveBeforeAvailableAdGroup_interstitialIgnored()
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:00:18.000Z\","
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
+ "\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.000Z\n"
+ "#EXTINF:6,\nmain0.0.ts\n"
+ "#EXTINF:6,\nmain1.0.ts\n"
+ "#EXTINF:6,\nmain2.0.ts\n"
+ "#EXTINF:6,\nmain3.0.ts\n" // ad0-0 cue point: 21:00:18
+ "#EXTINF:6,\nmain4.0.ts\n"
+ "\n",
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:6\n"
+ "#EXT-X-MEDIA-SEQUENCE:1\n"
+ "#EXT-X-DATERANGE:"
+ "ID=\"ad1-0\","
+ "CLASS=\"com.apple.hls.interstitial\","
+ "START-DATE=\"2020-01-02T21:00:06.000Z\","
+ "X-ASSET-URI=\"http://example.com/media-1-0.m3u8\""
+ "\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:06.000Z\n"
+ "#EXTINF:6,\nmain1.0.ts\n" // ad1-0 cue point: 21:00:06
+ "#EXTINF:6,\nmain2.0.ts\n"
+ "#EXTINF:6,\nmain3.0.ts\n" // ad0-0 cue point: 21:00:18
+ "#EXTINF:6,\nmain4.0.ts\n"
+ "#EXTINF:6,\nmain5.0.ts\n"
+ "\n"))
.containsExactly(
new AdPlaybackState("adsId", 18_000_000L)
.withAdResumePositionUs(0)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false))
.inOrder();
}
@Test
public void handleContentTimelineChanged_attemptInsertionBehindLiveWindow_interstitialIgnored()
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:00:00.000Z\","
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
+ "\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:00:00.001Z\n"
+ "#EXTINF:6,\nmain0.0.ts\n"
+ "#EXTINF:6,\nmain1.0.ts\n"
+ "#EXTINF:6,\nmain2.0.ts\n"
+ "\n"))
.containsExactly(
new AdPlaybackState("adsId")
.withAdResumePositionUs(0)
.withLivePostrollPlaceholderAppended(/* isServerSideInserted= */ false))
.inOrder();
}
@Test
@ -832,8 +1293,8 @@ public class HlsInterstitialsAdsLoaderTest {
/* periodIndex= */ 0,
/* positionMs= */ 0L,
/* contentPositionMs= */ 0L,
/* adGroupIndex= */ -1,
/* adIndexInAdGroup= */ -1),
/* adGroupIndex= */ C.INDEX_UNSET,
/* adIndexInAdGroup= */ C.INDEX_UNSET),
DISCONTINUITY_REASON_AUTO_TRANSITION);
verify(mockAdsLoaderListener)
@ -854,14 +1315,22 @@ public class HlsInterstitialsAdsLoaderTest {
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, C.TIME_UNSET)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, /* contentResumeOffsetUs= */ 0)
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0")
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, "ad0-1")
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 1,
MediaItem.fromUri("http://example.com/media-0-1.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-1.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1));
}
@ -884,6 +1353,7 @@ public class HlsInterstitialsAdsLoaderTest {
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
+ "\n";
callHandleContentTimelineChangedAndCaptureAdPlaybackState(playlistString, adsLoader);
reset(mockEventListener);
ArgumentCaptor<Player.Listener> listener = ArgumentCaptor.forClass(Player.Listener.class);
when(mockPlayer.getCurrentTimeline())
.thenReturn(new FakeTimeline(adsMediaSourceWindowDefinition));
@ -911,7 +1381,11 @@ public class HlsInterstitialsAdsLoaderTest {
.withAvailableAdMediaItem(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
MediaItem.fromUri("http://example.com/media-0-0.m3u8"))
new MediaItem.Builder()
.setUri("http://example.com/media-0-0.m3u8")
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build())
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-1")
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
}
@ -1032,7 +1506,6 @@ public class HlsInterstitialsAdsLoaderTest {
ArgumentCaptor<Player.Listener> listener = ArgumentCaptor.forClass(Player.Listener.class);
InOrder inOrder = inOrder(mockPlayer);
inOrder.verify(mockPlayer).addListener(listener.capture());
inOrder.verify(mockPlayer).getCurrentTimeline();
inOrder.verifyNoMoreInteractions();
reset(mockPlayer);
@ -1059,7 +1532,6 @@ public class HlsInterstitialsAdsLoaderTest {
ArgumentCaptor<Player.Listener> listener = ArgumentCaptor.forClass(Player.Listener.class);
InOrder inOrder = inOrder(mockPlayer);
inOrder.verify(mockPlayer).addListener(listener.capture());
inOrder.verify(mockPlayer).getCurrentTimeline();
inOrder.verifyNoMoreInteractions();
reset(mockPlayer);
@ -1334,6 +1806,55 @@ public class HlsInterstitialsAdsLoaderTest {
verifyNoMoreInteractions(mockEventListener);
}
private List<AdPlaybackState> callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates(
HlsInterstitialsAdsLoader adsLoader,
boolean startAdsLoader,
long windowOffsetInFirstPeriodUs,
String... playlistStrings)
throws IOException {
if (startAdsLoader) {
// Set the player.
adsLoader.setPlayer(mockPlayer);
// Start the ad.
adsLoader.start(
adsMediaSource, adTagDataSpec, "adsId", mockAdViewProvider, mockEventListener);
}
HlsPlaylistParser hlsPlaylistParser = new HlsPlaylistParser();
long firstPlaylistStartTimeUs = C.TIME_UNSET;
for (String playlistString : playlistStrings) {
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist mediaPlaylist =
(HlsMediaPlaylist) hlsPlaylistParser.parse(Uri.EMPTY, inputStream);
if (firstPlaylistStartTimeUs == C.TIME_UNSET) {
firstPlaylistStartTimeUs = mediaPlaylist.startTimeUs;
}
HlsManifest hlsManifest = new HlsManifest(/* multivariantPlaylist= */ null, mediaPlaylist);
adsLoader.handleContentTimelineChanged(
adsMediaSource,
new FakeTimeline(
new Object[] {hlsManifest},
new TimelineWindowDefinition.Builder()
.setDynamic(true)
.setLive(true)
.setDurationUs(mediaPlaylist.durationUs)
.setDefaultPositionUs(mediaPlaylist.durationUs / 2)
.setWindowStartTimeUs(mediaPlaylist.startTimeUs)
.setWindowPositionInFirstPeriodUs(
windowOffsetInFirstPeriodUs
+ (mediaPlaylist.startTimeUs - firstPlaylistStartTimeUs))
.setMediaItem(contentMediaItem)
.build()));
}
ArgumentCaptor<AdPlaybackState> adPlaybackState =
ArgumentCaptor.forClass(AdPlaybackState.class);
verify(mockEventListener, atMost(playlistStrings.length))
.onAdPlaybackState(adPlaybackState.capture());
when(mockPlayer.getCurrentTimeline())
.thenReturn(new FakeTimeline(adsMediaSourceWindowDefinition));
return adPlaybackState.getAllValues();
}
private AdPlaybackState callHandleContentTimelineChangedAndCaptureAdPlaybackState(
String playlistString, HlsInterstitialsAdsLoader adsLoader) throws IOException {
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));