Switch the IMA extension to use in-period ads

This also adds support for seeking in periods with midroll ads.

Remove Timeline.Period.isAd.

Issue: #2617

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=160510702
This commit is contained in:
andrewlewis 2017-06-29 04:22:22 -07:00 committed by Oliver Woodman
parent 6509dce6b7
commit 1f815db367
12 changed files with 334 additions and 719 deletions

View File

@ -36,9 +36,6 @@ section of the app.
This is a preview version with some known issues: This is a preview version with some known issues:
* Seeking is not yet ad aware. This means that it's possible to seek back into
ads that have already been played, and also seek past midroll ads without
them being played. Seeking will be made ad aware for the first stable release.
* Midroll ads are not yet fully supported. `playAd` and `AD_STARTED` events are * Midroll ads are not yet fully supported. `playAd` and `AD_STARTED` events are
sometimes delayed, meaning that midroll ads take a long time to start and the sometimes delayed, meaning that midroll ads take a long time to start and the
ad overlay does not show immediately. ad overlay does not show immediately.

View File

@ -1,275 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.ima;
import android.util.Pair;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
/**
* A {@link Timeline} for {@link ImaAdsMediaSource}.
*/
/* package */ final class AdTimeline extends Timeline {
private static final Object AD_ID = new Object();
/**
* Builder for ad timelines.
*/
public static final class Builder {
private final Timeline contentTimeline;
private final long contentDurationUs;
private final ArrayList<Boolean> isAd;
private final ArrayList<Ad> ads;
private final ArrayList<Long> startTimesUs;
private final ArrayList<Long> endTimesUs;
private final ArrayList<Object> uids;
/**
* Creates a new ad timeline builder using the specified {@code contentTimeline} as the timeline
* of the content within which to insert ad breaks.
*
* @param contentTimeline The timeline of the content within which to insert ad breaks.
*/
public Builder(Timeline contentTimeline) {
this.contentTimeline = contentTimeline;
contentDurationUs = contentTimeline.getPeriod(0, new Period()).durationUs;
isAd = new ArrayList<>();
ads = new ArrayList<>();
startTimesUs = new ArrayList<>();
endTimesUs = new ArrayList<>();
uids = new ArrayList<>();
}
/**
* Adds an ad period. Each individual ad in an ad pod is represented by a separate ad period.
*
* @param ad The {@link Ad} instance representing the ad break, or {@code null} if not known.
* @param adBreakIndex The index of the ad break that contains the ad in the timeline.
* @param adIndexInAdBreak The index of the ad in its ad break.
* @param durationUs The duration of the ad, in microseconds. May be {@link C#TIME_UNSET}.
* @return The builder.
*/
public Builder addAdPeriod(Ad ad, int adBreakIndex, int adIndexInAdBreak, long durationUs) {
isAd.add(true);
ads.add(ad);
startTimesUs.add(0L);
endTimesUs.add(durationUs);
uids.add(Pair.create(adBreakIndex, adIndexInAdBreak));
return this;
}
/**
* Adds a content period.
*
* @param startTimeUs The start time of the period relative to the start of the content
* timeline, in microseconds.
* @param endTimeUs The end time of the period relative to the start of the content timeline, in
* microseconds. May be {@link C#TIME_UNSET} to include the rest of the content.
* @return The builder.
*/
public Builder addContent(long startTimeUs, long endTimeUs) {
ads.add(null);
isAd.add(false);
startTimesUs.add(startTimeUs);
endTimesUs.add(endTimeUs == C.TIME_UNSET ? contentDurationUs : endTimeUs);
uids.add(Pair.create(startTimeUs, endTimeUs));
return this;
}
/**
* Builds and returns the ad timeline.
*/
public AdTimeline build() {
int periodCount = uids.size();
Assertions.checkState(periodCount > 0);
Ad[] ads = new Ad[periodCount];
boolean[] isAd = new boolean[periodCount];
long[] startTimesUs = new long[periodCount];
long[] endTimesUs = new long[periodCount];
for (int i = 0; i < periodCount; i++) {
ads[i] = this.ads.get(i);
isAd[i] = this.isAd.get(i);
startTimesUs[i] = this.startTimesUs.get(i);
endTimesUs[i] = this.endTimesUs.get(i);
}
Object[] uids = this.uids.toArray(new Object[periodCount]);
return new AdTimeline(contentTimeline, isAd, ads, startTimesUs, endTimesUs, uids);
}
}
private final Period contentPeriod;
private final Window contentWindow;
private final boolean[] isAd;
private final Ad[] ads;
private final long[] startTimesUs;
private final long[] endTimesUs;
private final Object[] uids;
private AdTimeline(Timeline contentTimeline, boolean[] isAd, Ad[] ads, long[] startTimesUs,
long[] endTimesUs, Object[] uids) {
contentWindow = contentTimeline.getWindow(0, new Window(), true);
contentPeriod = contentTimeline.getPeriod(0, new Period(), true);
this.isAd = isAd;
this.ads = ads;
this.startTimesUs = startTimesUs;
this.endTimesUs = endTimesUs;
this.uids = uids;
}
/**
* Returns whether the period at {@code index} contains ad media.
*/
public boolean isPeriodAd(int index) {
return isAd[index];
}
/**
* Returns the duration of the content within which ads have been inserted, in microseconds.
*/
public long getContentDurationUs() {
return contentPeriod.durationUs;
}
/**
* Returns the start time of the period at {@code periodIndex} relative to the start of the
* content, in microseconds.
*
* @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not a content
* period.
*/
public long getContentStartTimeUs(int periodIndex) {
Assertions.checkArgument(!isAd[periodIndex]);
return startTimesUs[periodIndex];
}
/**
* Returns the end time of the period at {@code periodIndex} relative to the start of the content,
* in microseconds.
*
* @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not a content
* period.
*/
public long getContentEndTimeUs(int periodIndex) {
Assertions.checkArgument(!isAd[periodIndex]);
return endTimesUs[periodIndex];
}
/**
* Returns the index of the ad break to which the period at {@code periodIndex} belongs.
*
* @param periodIndex The period index.
* @return The index of the ad break to which the period belongs.
* @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not an ad.
*/
public int getAdBreakIndex(int periodIndex) {
Assertions.checkArgument(isAd[periodIndex]);
int adBreakIndex = 0;
for (int i = 1; i < periodIndex; i++) {
if (!isAd[i] && isAd[i - 1]) {
adBreakIndex++;
}
}
return adBreakIndex;
}
/**
* Returns the index of the ad at {@code periodIndex} in its ad break.
*
* @param periodIndex The period index.
* @return The index of the ad at {@code periodIndex} in its ad break.
* @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not an ad.
*/
public int getAdIndexInAdBreak(int periodIndex) {
Assertions.checkArgument(isAd[periodIndex]);
int adIndex = 0;
for (int i = 0; i < periodIndex; i++) {
if (isAd[i]) {
adIndex++;
} else {
adIndex = 0;
}
}
return adIndex;
}
@Override
public int getWindowCount() {
return uids.length;
}
@Override
public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
if (repeatMode == ExoPlayer.REPEAT_MODE_ONE) {
repeatMode = ExoPlayer.REPEAT_MODE_ALL;
}
return super.getNextWindowIndex(windowIndex, repeatMode);
}
@Override
public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) {
if (repeatMode == ExoPlayer.REPEAT_MODE_ONE) {
repeatMode = ExoPlayer.REPEAT_MODE_ALL;
}
return super.getPreviousWindowIndex(windowIndex, repeatMode);
}
@Override
public Window getWindow(int index, Window window, boolean setIds,
long defaultPositionProjectionUs) {
long startTimeUs = startTimesUs[index];
long durationUs = endTimesUs[index] - startTimeUs;
if (isAd[index]) {
window.set(ads[index], C.TIME_UNSET, C.TIME_UNSET, false, false, 0L, durationUs, index, index,
0L);
} else {
window.set(contentWindow.id, contentWindow.presentationStartTimeMs + C.usToMs(startTimeUs),
contentWindow.windowStartTimeMs + C.usToMs(startTimeUs), contentWindow.isSeekable, false,
0L, durationUs, index, index, 0L);
}
return window;
}
@Override
public int getPeriodCount() {
return uids.length;
}
@Override
public Period getPeriod(int index, Period period, boolean setIds) {
Object id = setIds ? (isAd[index] ? AD_ID : contentPeriod.id) : null;
return period.set(id, uids[index], index, endTimesUs[index] - startTimesUs[index], 0,
isAd[index]);
}
@Override
public int getIndexOfPeriod(Object uid) {
for (int i = 0; i < uids.length; i++) {
if (Util.areEqual(uid, uids[i])) {
return i;
}
}
return C.INDEX_UNSET;
}
}

View File

@ -66,36 +66,35 @@ import java.util.List;
public interface EventListener { public interface EventListener {
/** /**
* Called when the timestamps of ad breaks are known. * Called when the times of ad groups are known.
* *
* @param adBreakTimesUs The times of ad breaks, in microseconds. * @param adGroupTimesUs The times of ad groups, in microseconds.
*/ */
void onAdBreakTimesUsLoaded(long[] adBreakTimesUs); void onAdGroupTimesUsLoaded(long[] adGroupTimesUs);
/**
* Called when an ad group has been played to the end.
*
* @param adGroupIndex The index of the ad group.
*/
void onAdGroupPlayedToEnd(int adGroupIndex);
/** /**
* Called when the URI for the media of an ad has been loaded. * Called when the URI for the media of an ad has been loaded.
* *
* @param adBreakIndex The index of the ad break containing the ad with the media URI. * @param adGroupIndex The index of the ad group containing the ad with the media URI.
* @param adIndexInAdBreak The index of the ad in its ad break. * @param adIndexInAdGroup The index of the ad in its ad group.
* @param uri The URI for the ad's media. * @param uri The URI for the ad's media.
*/ */
void onUriLoaded(int adBreakIndex, int adIndexInAdBreak, Uri uri); void onAdUriLoaded(int adGroupIndex, int adIndexInAdGroup, Uri uri);
/** /**
* Called when the {@link Ad} instance for a specified ad has been loaded. * Called when an ad group has loaded.
* *
* @param adBreakIndex The index of the ad break containing the ad. * @param adGroupIndex The index of the ad group containing the ad.
* @param adIndexInAdBreak The index of the ad in its ad break. * @param adCountInAdGroup The number of ads in the ad group.
* @param ad The {@link Ad} instance for the ad.
*/ */
void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad); void onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup);
/**
* Called when the specified ad break has been played to the end.
*
* @param adBreakIndex The index of the ad break.
*/
void onAdBreakPlayedToEnd(int adBreakIndex);
/** /**
* Called when there was an error loading ads. * Called when there was an error loading ads.
@ -127,51 +126,45 @@ import java.util.List;
private final AdsLoader adsLoader; private final AdsLoader adsLoader;
private AdsManager adsManager; private AdsManager adsManager;
private AdTimeline adTimeline; private long[] adGroupTimesUs;
private int[] adsLoadedInAdGroup;
private Timeline timeline;
private long contentDurationMs; private long contentDurationMs;
private int lastContentPeriodIndex;
private int playerPeriodIndex;
private boolean released; private boolean released;
// Fields tracking IMA's state. // Fields tracking IMA's state.
/** /**
* The index of the current ad break that IMA is loading. * The index of the current ad group that IMA is loading.
*/ */
private int adBreakIndex; private int adGroupIndex;
/** /**
* The index of the ad within its ad break, in {@link #loadAd(String)}. * If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #playAd()} and not
*/
private int adIndexInAdBreak;
/**
* The total number of ads in the current ad break, or {@link C#INDEX_UNSET} if unknown.
*/
private int adCountInAdBreak;
/**
* Tracks the period currently being played in IMA's model of playback.
*/
private int imaPeriodIndex;
/**
* Whether the period at {@link #imaPeriodIndex} is an ad.
*/
private boolean isAdDisplayed;
/**
* Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback.
*/
private boolean sentContentComplete;
/**
* If {@link #isAdDisplayed} is set, stores whether IMA has called {@link #playAd()} and not
* {@link #stopAd()}. * {@link #stopAd()}.
*/ */
private boolean playingAd; private boolean playingAd;
/** /**
* If {@link #isAdDisplayed} is set, stores whether IMA has called {@link #pauseAd()} since a * If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #pauseAd()} since
* preceding call to {@link #playAd()} for the current ad. * a preceding call to {@link #playAd()} for the current ad.
*/ */
private boolean pausedInAd; private boolean pausedInAd;
/**
* Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback.
*/
private boolean sentContentComplete;
// Fields tracking the player/loader state.
/**
* If the player is playing an ad, stores the ad group index. {@link C#INDEX_UNSET} otherwise.
*/
private int playingAdGroupIndex;
/**
* If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET}
* otherwise.
*/
private int playingAdIndexInAdGroup;
/** /**
* If a content period has finished but IMA has not yet sent an ad event with * If a content period has finished but IMA has not yet sent an ad event with
* {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of * {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of
@ -179,6 +172,14 @@ import java.util.List;
* determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise.
*/ */
private long fakeContentProgressElapsedRealtimeMs; private long fakeContentProgressElapsedRealtimeMs;
/**
* Stores the pending content position when a seek operation was intercepted to play an ad.
*/
private long pendingContentPositionMs;
/**
* Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA.
*/
private boolean sentPendingContentPositionMs;
/** /**
* Creates a new IMA ads loader. * Creates a new IMA ads loader.
@ -190,8 +191,7 @@ import java.util.List;
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
* @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to
* use the default settings. If set, the player type and version fields may be overwritten. * use the default settings. If set, the player type and version fields may be overwritten.
* @param player The player instance that will play the loaded ad schedule. The player's timeline * @param player The player instance that will play the loaded ad schedule.
* must be an {@link AdTimeline} matching the loaded ad schedule.
* @param eventListener Listener for ad loader events. * @param eventListener Listener for ad loader events.
*/ */
public ImaAdsLoader(Context context, Uri adTagUri, ViewGroup adUiViewGroup, public ImaAdsLoader(Context context, Uri adTagUri, ViewGroup adUiViewGroup,
@ -201,9 +201,10 @@ import java.util.List;
period = new Timeline.Period(); period = new Timeline.Period();
adCallbacks = new ArrayList<>(1); adCallbacks = new ArrayList<>(1);
lastContentPeriodIndex = C.INDEX_UNSET;
adCountInAdBreak = C.INDEX_UNSET;
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
pendingContentPositionMs = C.TIME_UNSET;
adGroupIndex = C.INDEX_UNSET;
contentDurationMs = C.TIME_UNSET;
player.addListener(this); player.addListener(this);
@ -262,13 +263,16 @@ import java.util.List;
Log.d(TAG, "Initialized without preloading"); Log.d(TAG, "Initialized without preloading");
} }
} }
eventListener.onAdBreakTimesUsLoaded(getAdBreakTimesUs(adsManager.getAdCuePoints())); adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
adsLoadedInAdGroup = new int[adGroupTimesUs.length];
eventListener.onAdGroupTimesUsLoaded(adGroupTimesUs);
} }
// AdEvent.AdEventListener implementation. // AdEvent.AdEventListener implementation.
@Override @Override
public void onAdEvent(AdEvent adEvent) { public void onAdEvent(AdEvent adEvent) {
Ad ad = adEvent.getAd();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onAdEvent " + adEvent.getType()); Log.d(TAG, "onAdEvent " + adEvent.getType());
} }
@ -278,20 +282,18 @@ import java.util.List;
} }
switch (adEvent.getType()) { switch (adEvent.getType()) {
case LOADED: case LOADED:
adsManager.start(); // The ad position is not always accurate when using preloading. See [Internal: b/62613240].
break;
case STARTED:
// Note: This event is sometimes delivered several seconds after playAd is called.
// See [Internal: b/37775441].
Ad ad = adEvent.getAd();
AdPodInfo adPodInfo = ad.getAdPodInfo(); AdPodInfo adPodInfo = ad.getAdPodInfo();
adCountInAdBreak = adPodInfo.getTotalAds(); int podIndex = adPodInfo.getPodIndex();
adGroupIndex = podIndex == -1 ? adGroupTimesUs.length - 1 : podIndex;
int adPosition = adPodInfo.getAdPosition(); int adPosition = adPodInfo.getAdPosition();
eventListener.onAdLoaded(adBreakIndex, adPosition - 1, ad); int adCountInAdGroup = adPodInfo.getTotalAds();
adsManager.start();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Started ad " + adPosition + " of " + adCountInAdBreak + " in ad break " Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group "
+ adBreakIndex); + adGroupIndex);
} }
eventListener.onAdGroupLoaded(adGroupIndex, adCountInAdGroup);
break; break;
case CONTENT_PAUSE_REQUESTED: case CONTENT_PAUSE_REQUESTED:
// After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
@ -325,40 +327,41 @@ import java.util.List;
@Override @Override
public VideoProgressUpdate getContentProgress() { public VideoProgressUpdate getContentProgress() {
if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { if (pendingContentPositionMs != C.TIME_UNSET) {
long contentEndTimeMs = C.usToMs(adTimeline.getContentEndTimeUs(imaPeriodIndex)); sentPendingContentPositionMs = true;
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs);
return new VideoProgressUpdate(contentEndTimeMs + elapsedSinceEndMs, contentDurationMs);
} }
if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
if (adTimeline == null || isAdDisplayed || imaPeriodIndex != playerPeriodIndex long adGroupTimeMs = C.usToMs(adGroupTimesUs[adGroupIndex]);
|| contentDurationMs == C.TIME_UNSET) { if (adGroupTimeMs == C.TIME_END_OF_SOURCE) {
adGroupTimeMs = contentDurationMs;
}
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
return new VideoProgressUpdate(adGroupTimeMs + elapsedSinceEndMs, contentDurationMs);
}
if (player.isPlayingAd() || contentDurationMs == C.TIME_UNSET) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY; return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} }
checkForContentComplete(); return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs);
long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex))
+ player.getCurrentPosition();
return new VideoProgressUpdate(positionMs, contentDurationMs);
} }
// VideoAdPlayer implementation. // VideoAdPlayer implementation.
@Override @Override
public VideoProgressUpdate getAdProgress() { public VideoProgressUpdate getAdProgress() {
if (adTimeline == null || !isAdDisplayed || imaPeriodIndex != playerPeriodIndex if (!player.isPlayingAd()) {
|| adTimeline.getPeriod(imaPeriodIndex, period).getDurationUs() == C.TIME_UNSET) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY; return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} }
return new VideoProgressUpdate(player.getCurrentPosition(), period.getDurationMs()); return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration());
} }
@Override @Override
public void loadAd(String adUriString) { public void loadAd(String adUriString) {
int adIndexInAdGroup = adsLoadedInAdGroup[adGroupIndex]++;
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "loadAd at index " + adIndexInAdBreak + " in ad break " + adBreakIndex); Log.d(TAG, "loadAd at index " + adIndexInAdGroup + " in ad group " + adGroupIndex);
} }
eventListener.onUriLoaded(adBreakIndex, adIndexInAdBreak, Uri.parse(adUriString)); eventListener.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString));
adIndexInAdBreak++;
} }
@Override @Override
@ -376,7 +379,6 @@ import java.util.List;
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "playAd"); Log.d(TAG, "playAd");
} }
Assertions.checkState(isAdDisplayed);
if (playingAd && !pausedInAd) { if (playingAd && !pausedInAd) {
// Work around an issue where IMA does not always call stopAd before resuming content. // Work around an issue where IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028]. // See [Internal: b/38354028].
@ -443,18 +445,12 @@ import java.util.List;
// The player is being re-prepared and this source will be released. // The player is being re-prepared and this source will be released.
return; return;
} }
if (adTimeline == null) { Assertions.checkArgument(timeline.getPeriodCount() == 1);
// TODO: Handle initial seeks after the first period. this.timeline = timeline;
isAdDisplayed = timeline.getPeriod(0, period).isAd; contentDurationMs = C.usToMs(timeline.getPeriod(0, period).durationUs);
imaPeriodIndex = 0; if (player.isPlayingAd()) {
player.seekTo(0, 0); playingAdGroupIndex = player.getCurrentAdGroupIndex();
} playingAdIndexInAdGroup = player.getCurrentAdIndexInAdGroup();
adTimeline = (AdTimeline) timeline;
contentDurationMs = C.usToMs(adTimeline.getContentDurationUs());
lastContentPeriodIndex = adTimeline.getPeriodCount() - 1;
while (adTimeline.isPeriodAd(lastContentPeriodIndex)) {
// All timelines have at least one content period.
lastContentPeriodIndex--;
} }
} }
@ -470,9 +466,9 @@ import java.util.List;
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) { if (!playingAd && playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) {
checkForContentComplete(); checkForContentComplete();
} else if (playbackState == ExoPlayer.STATE_ENDED && isAdDisplayed) { } else if (playingAd && playbackState == ExoPlayer.STATE_ENDED) {
// IMA is waiting for the ad playback to finish so invoke the callback now. // IMA is waiting for the ad playback to finish so invoke the callback now.
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
for (VideoAdPlayerCallback callback : adCallbacks) { for (VideoAdPlayerCallback callback : adCallbacks) {
@ -488,7 +484,7 @@ import java.util.List;
@Override @Override
public void onPlayerError(ExoPlaybackException error) { public void onPlayerError(ExoPlaybackException error) {
if (isAdDisplayed && adTimeline.isPeriodAd(playerPeriodIndex)) { if (player.isPlayingAd()) {
for (VideoAdPlayerCallback callback : adCallbacks) { for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onError(); callback.onError();
} }
@ -497,23 +493,41 @@ import java.util.List;
@Override @Override
public void onPositionDiscontinuity() { public void onPositionDiscontinuity() {
if (player.getCurrentPeriodIndex() == playerPeriodIndex + 1) { if (!player.isPlayingAd() && playingAdGroupIndex == C.INDEX_UNSET) {
if (isAdDisplayed) { long positionUs = C.msToUs(player.getCurrentPosition());
// IMA is waiting for the ad playback to finish so invoke the callback now. int adGroupIndex = timeline.getPeriod(0, period).getAdGroupIndexForPositionUs(positionUs);
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. if (adGroupIndex != C.INDEX_UNSET) {
for (VideoAdPlayerCallback callback : adCallbacks) { sentPendingContentPositionMs = false;
callback.onEnded(); pendingContentPositionMs = player.getCurrentPosition();
} }
} else { return;
player.setPlayWhenReady(false); }
if (imaPeriodIndex == playerPeriodIndex) {
// IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. boolean adFinished = (!player.isPlayingAd() && playingAdGroupIndex != C.INDEX_UNSET)
Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); || (player.isPlayingAd() && playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup());
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); if (adFinished) {
// IMA is waiting for the ad playback to finish so invoke the callback now.
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onEnded();
}
}
if (player.isPlayingAd() && playingAdGroupIndex == C.INDEX_UNSET) {
player.setPlayWhenReady(false);
// IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position.
Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET);
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
if (adGroupIndex == adGroupTimesUs.length - 1) {
adsLoader.contentComplete();
if (DEBUG) {
Log.d(TAG, "adsLoader.contentComplete");
} }
} }
} }
playerPeriodIndex = player.getCurrentPeriodIndex(); boolean isPlayingAd = player.isPlayingAd();
playingAdGroupIndex = isPlayingAd ? player.getCurrentAdGroupIndex() : C.INDEX_UNSET;
playingAdIndexInAdGroup = isPlayingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET;
} }
@Override @Override
@ -527,70 +541,42 @@ import java.util.List;
* Resumes the player, ensuring the current period is a content period by seeking if necessary. * Resumes the player, ensuring the current period is a content period by seeking if necessary.
*/ */
private void resumeContentInternal() { private void resumeContentInternal() {
if (adTimeline != null) { if (contentDurationMs != C.TIME_UNSET) {
if (imaPeriodIndex < lastContentPeriodIndex) { if (playingAd) {
if (playingAd) { // Work around an issue where IMA does not always call stopAd before resuming content.
// Work around an issue where IMA does not always call stopAd before resuming content. // See [Internal: b/38354028].
// See [Internal: b/38354028]. if (DEBUG) {
if (DEBUG) { Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
}
stopAdInternal();
} }
while (adTimeline.isPeriodAd(imaPeriodIndex)) { stopAdInternal();
imaPeriodIndex++;
}
synchronizePlayerToIma();
} }
} }
player.setPlayWhenReady(true); player.setPlayWhenReady(true);
clearFlags();
} }
/**
* Pauses the player, and ensures that the current period is an ad period by seeking if necessary.
*/
private void pauseContentInternal() { private void pauseContentInternal() {
if (sentPendingContentPositionMs) {
pendingContentPositionMs = C.TIME_UNSET;
sentPendingContentPositionMs = false;
}
// IMA is requesting to pause content, so stop faking the content position. // IMA is requesting to pause content, so stop faking the content position.
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
if (adTimeline != null && !isAdDisplayed) {
// Seek to the next ad.
while (!adTimeline.isPeriodAd(imaPeriodIndex)) {
imaPeriodIndex++;
}
synchronizePlayerToIma();
} else {
// IMA is sending an initial CONTENT_PAUSE_REQUESTED before a pre-roll ad.
Assertions.checkState(playerPeriodIndex == 0 && imaPeriodIndex == 0);
}
player.setPlayWhenReady(false); player.setPlayWhenReady(false);
clearFlags();
} }
/**
* Stops the currently playing ad, seeking to the next content period if there is one. May only be
* called when {@link #playingAd} is {@code true}.
*/
private void stopAdInternal() { private void stopAdInternal() {
Assertions.checkState(playingAd); Assertions.checkState(playingAd);
if (imaPeriodIndex != adTimeline.getPeriodCount() - 1) { player.setPlayWhenReady(false);
player.setPlayWhenReady(false); if (!player.isPlayingAd()) {
imaPeriodIndex++; eventListener.onAdGroupPlayedToEnd(adGroupIndex);
if (!adTimeline.isPeriodAd(imaPeriodIndex)) { adGroupIndex = C.INDEX_UNSET;
eventListener.onAdBreakPlayedToEnd(adBreakIndex);
adBreakIndex++;
adIndexInAdBreak = 0;
}
synchronizePlayerToIma();
} else {
eventListener.onAdBreakPlayedToEnd(adTimeline.getAdBreakIndex(imaPeriodIndex));
} }
clearFlags();
} }
private void synchronizePlayerToIma() { private void clearFlags() {
if (playerPeriodIndex != imaPeriodIndex) {
player.seekTo(imaPeriodIndex, 0);
}
isAdDisplayed = adTimeline.isPeriodAd(imaPeriodIndex);
// If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until // If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until
// the content is resumed. // the content is resumed.
playingAd = false; playingAd = false;
@ -598,14 +584,9 @@ import java.util.List;
} }
private void checkForContentComplete() { private void checkForContentComplete() {
if (adTimeline == null || isAdDisplayed || sentContentComplete) { if (contentDurationMs != C.TIME_UNSET
return; && player.getCurrentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
} && !sentContentComplete) {
long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex))
+ player.getCurrentPosition();
if (playerPeriodIndex == lastContentPeriodIndex
&& positionMs + END_OF_CONTENT_POSITION_THRESHOLD_MS
>= C.usToMs(adTimeline.getContentEndTimeUs(playerPeriodIndex))) {
adsLoader.contentComplete(); adsLoader.contentComplete();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "adsLoader.contentComplete"); Log.d(TAG, "adsLoader.contentComplete");
@ -614,19 +595,20 @@ import java.util.List;
} }
} }
private static long[] getAdBreakTimesUs(List<Float> cuePoints) { private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
if (cuePoints.isEmpty()) { if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad break. // If no cue points are specified, there is a preroll ad.
return new long[] {0}; return new long[] {0};
} }
int count = cuePoints.size(); int count = cuePoints.size();
long[] adBreakTimesUs = new long[count]; long[] adGroupTimesUs = new long[count];
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
double cuePoint = cuePoints.get(i); double cuePoint = cuePoints.get(i);
adBreakTimesUs[i] = cuePoint == -1.0 ? C.TIME_UNSET : (long) (C.MICROS_PER_SECOND * cuePoint); adGroupTimesUs[i] =
cuePoint == -1.0 ? C.TIME_END_OF_SOURCE : (long) (C.MICROS_PER_SECOND * cuePoint);
} }
return adBreakTimesUs; return adGroupTimesUs;
} }
} }

View File

@ -20,20 +20,14 @@ import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdPodInfo;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.ClippingMediaPeriod;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -56,7 +50,8 @@ public final class ImaAdsMediaSource implements MediaSource {
private final ImaSdkSettings imaSdkSettings; private final ImaSdkSettings imaSdkSettings;
private final Handler mainHandler; private final Handler mainHandler;
private final AdListener adLoaderListener; private final AdListener adLoaderListener;
private final Map<MediaPeriod, MediaSource> mediaSourceByMediaPeriod; private final Map<MediaPeriod, MediaSource> adMediaSourceByMediaPeriod;
private final Timeline.Period period;
private Handler playerHandler; private Handler playerHandler;
private ExoPlayer player; private ExoPlayer player;
@ -65,13 +60,12 @@ public final class ImaAdsMediaSource implements MediaSource {
// Accessed on the player thread. // Accessed on the player thread.
private Timeline contentTimeline; private Timeline contentTimeline;
private Object contentManifest; private Object contentManifest;
private long[] adBreakTimesUs; private long[] adGroupTimesUs;
private boolean[] playedAdBreak; private boolean[] hasPlayedAdGroup;
private Ad[][] adBreakAds; private int[] adCounts;
private Timeline[][] adBreakTimelines; private MediaSource[][] adGroupMediaSources;
private MediaSource[][] adBreakMediaSources; private boolean[][] isAdAvailable;
private DeferredMediaPeriod[][] adBreakDeferredMediaPeriods; private long[][] adDurationsUs;
private AdTimeline timeline;
private MediaSource.Listener listener; private MediaSource.Listener listener;
private IOException adLoadError; private IOException adLoadError;
@ -120,8 +114,11 @@ public final class ImaAdsMediaSource implements MediaSource {
this.imaSdkSettings = imaSdkSettings; this.imaSdkSettings = imaSdkSettings;
mainHandler = new Handler(Looper.getMainLooper()); mainHandler = new Handler(Looper.getMainLooper());
adLoaderListener = new AdListener(); adLoaderListener = new AdListener();
mediaSourceByMediaPeriod = new HashMap<>(); adMediaSourceByMediaPeriod = new HashMap<>();
adBreakMediaSources = new MediaSource[0][]; period = new Timeline.Period();
adGroupMediaSources = new MediaSource[0][];
isAdAvailable = new boolean[0][];
adDurationsUs = new long[0][];
} }
@Override @Override
@ -151,7 +148,7 @@ public final class ImaAdsMediaSource implements MediaSource {
throw adLoadError; throw adLoadError;
} }
contentMediaSource.maybeThrowSourceInfoRefreshError(); contentMediaSource.maybeThrowSourceInfoRefreshError();
for (MediaSource[] mediaSources : adBreakMediaSources) { for (MediaSource[] mediaSources : adGroupMediaSources) {
for (MediaSource mediaSource : mediaSources) { for (MediaSource mediaSource : mediaSources) {
mediaSource.maybeThrowSourceInfoRefreshError(); mediaSource.maybeThrowSourceInfoRefreshError();
} }
@ -160,49 +157,23 @@ public final class ImaAdsMediaSource implements MediaSource {
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
int index = id.periodIndex; if (id.isAd()) {
if (timeline.isPeriodAd(index)) { MediaSource mediaSource = adGroupMediaSources[id.adGroupIndex][id.adIndexInAdGroup];
int adBreakIndex = timeline.getAdBreakIndex(index); MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator);
int adIndexInAdBreak = timeline.getAdIndexInAdBreak(index); adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource);
if (adIndexInAdBreak >= adBreakMediaSources[adBreakIndex].length) { return mediaPeriod;
DeferredMediaPeriod deferredPeriod = new DeferredMediaPeriod(0, allocator);
if (adIndexInAdBreak >= adBreakDeferredMediaPeriods[adBreakIndex].length) {
adBreakDeferredMediaPeriods[adBreakIndex] = Arrays.copyOf(
adBreakDeferredMediaPeriods[adBreakIndex], adIndexInAdBreak + 1);
}
adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak] = deferredPeriod;
return deferredPeriod;
}
MediaSource adBreakMediaSource = adBreakMediaSources[adBreakIndex][adIndexInAdBreak];
MediaPeriod adBreakMediaPeriod =
adBreakMediaSource.createPeriod(new MediaPeriodId(0), allocator);
mediaSourceByMediaPeriod.put(adBreakMediaPeriod, adBreakMediaSource);
return adBreakMediaPeriod;
} else { } else {
long startUs = timeline.getContentStartTimeUs(index); return contentMediaSource.createPeriod(id, allocator);
long endUs = timeline.getContentEndTimeUs(index);
MediaPeriod contentMediaPeriod =
contentMediaSource.createPeriod(new MediaPeriodId(0), allocator);
ClippingMediaPeriod clippingPeriod = new ClippingMediaPeriod(contentMediaPeriod, true);
clippingPeriod.setClipping(startUs, endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE : endUs);
mediaSourceByMediaPeriod.put(contentMediaPeriod, contentMediaSource);
return clippingPeriod;
} }
} }
@Override @Override
public void releasePeriod(MediaPeriod mediaPeriod) { public void releasePeriod(MediaPeriod mediaPeriod) {
if (mediaPeriod instanceof DeferredMediaPeriod) { if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) {
mediaPeriod = ((DeferredMediaPeriod) mediaPeriod).mediaPeriod; adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod);
if (mediaPeriod == null) { } else {
// Nothing to do. contentMediaSource.releasePeriod(mediaPeriod);
return;
}
} else if (mediaPeriod instanceof ClippingMediaPeriod) {
mediaPeriod = ((ClippingMediaPeriod) mediaPeriod).mediaPeriod;
} }
mediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod);
} }
@Override @Override
@ -210,7 +181,7 @@ public final class ImaAdsMediaSource implements MediaSource {
released = true; released = true;
adLoadError = null; adLoadError = null;
contentMediaSource.releaseSource(); contentMediaSource.releaseSource();
for (MediaSource[] mediaSources : adBreakMediaSources) { for (MediaSource[] mediaSources : adGroupMediaSources) {
for (MediaSource mediaSource : mediaSources) { for (MediaSource mediaSource : mediaSources) {
mediaSource.releaseSource(); mediaSource.releaseSource();
} }
@ -229,19 +200,19 @@ public final class ImaAdsMediaSource implements MediaSource {
// Internal methods. // Internal methods.
private void onAdBreakTimesUsLoaded(long[] adBreakTimesUs) { private void onAdGroupTimesUsLoaded(long[] adGroupTimesUs) {
Assertions.checkState(this.adBreakTimesUs == null); Assertions.checkState(this.adGroupTimesUs == null);
this.adBreakTimesUs = adBreakTimesUs; int adGroupCount = adGroupTimesUs.length;
int adBreakCount = adBreakTimesUs.length; this.adGroupTimesUs = adGroupTimesUs;
adBreakAds = new Ad[adBreakCount][]; hasPlayedAdGroup = new boolean[adGroupCount];
Arrays.fill(adBreakAds, new Ad[0]); adCounts = new int[adGroupCount];
adBreakTimelines = new Timeline[adBreakCount][]; Arrays.fill(adCounts, C.LENGTH_UNSET);
Arrays.fill(adBreakTimelines, new Timeline[0]); adGroupMediaSources = new MediaSource[adGroupCount][];
adBreakMediaSources = new MediaSource[adBreakCount][]; Arrays.fill(adGroupMediaSources, new MediaSource[0]);
Arrays.fill(adBreakMediaSources, new MediaSource[0]); isAdAvailable = new boolean[adGroupCount][];
adBreakDeferredMediaPeriods = new DeferredMediaPeriod[adBreakCount][]; Arrays.fill(isAdAvailable, new boolean[0]);
Arrays.fill(adBreakDeferredMediaPeriods, new DeferredMediaPeriod[0]); adDurationsUs = new long[adGroupCount][];
playedAdBreak = new boolean[adBreakCount]; Arrays.fill(adDurationsUs, new long[0]);
maybeUpdateSourceInfo(); maybeUpdateSourceInfo();
} }
@ -251,98 +222,51 @@ public final class ImaAdsMediaSource implements MediaSource {
maybeUpdateSourceInfo(); maybeUpdateSourceInfo();
} }
private void onAdUriLoaded(final int adBreakIndex, final int adIndexInAdBreak, Uri uri) { private void onAdGroupPlayedToEnd(int adGroupIndex) {
hasPlayedAdGroup[adGroupIndex] = true;
maybeUpdateSourceInfo();
}
private void onAdUriLoaded(final int adGroupIndex, final int adIndexInAdGroup, Uri uri) {
MediaSource adMediaSource = new ExtractorMediaSource(uri, dataSourceFactory, MediaSource adMediaSource = new ExtractorMediaSource(uri, dataSourceFactory,
new DefaultExtractorsFactory(), mainHandler, adLoaderListener); new DefaultExtractorsFactory(), mainHandler, adLoaderListener);
if (adBreakMediaSources[adBreakIndex].length <= adIndexInAdBreak) { int oldAdCount = adGroupMediaSources[adGroupIndex].length;
int adCount = adIndexInAdBreak + 1; if (adIndexInAdGroup >= oldAdCount) {
adBreakMediaSources[adBreakIndex] = Arrays.copyOf(adBreakMediaSources[adBreakIndex], adCount); int adCount = adIndexInAdGroup + 1;
adBreakTimelines[adBreakIndex] = Arrays.copyOf(adBreakTimelines[adBreakIndex], adCount); adGroupMediaSources[adGroupIndex] = Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount);
} isAdAvailable[adGroupIndex] = Arrays.copyOf(isAdAvailable[adGroupIndex], adCount);
adBreakMediaSources[adBreakIndex][adIndexInAdBreak] = adMediaSource; adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount);
if (adIndexInAdBreak < adBreakDeferredMediaPeriods[adBreakIndex].length Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET);
&& adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak] != null) {
adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].setMediaSource(
adBreakMediaSources[adBreakIndex][adIndexInAdBreak]);
mediaSourceByMediaPeriod.put(
adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].mediaPeriod, adMediaSource);
} }
adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource;
isAdAvailable[adGroupIndex][adIndexInAdGroup] = true;
adMediaSource.prepareSource(player, false, new Listener() { adMediaSource.prepareSource(player, false, new Listener() {
@Override @Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
onAdSourceInfoRefreshed(adBreakIndex, adIndexInAdBreak, timeline); onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline);
} }
}); });
} }
private void onAdSourceInfoRefreshed(int adBreakIndex, int adIndexInAdBreak, Timeline timeline) { private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) {
adBreakTimelines[adBreakIndex][adIndexInAdBreak] = timeline; Assertions.checkArgument(timeline.getPeriodCount() == 1);
adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs();
maybeUpdateSourceInfo(); maybeUpdateSourceInfo();
} }
private void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad) { private void onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup) {
if (adBreakAds[adBreakIndex].length <= adIndexInAdBreak) { if (adCounts[adGroupIndex] == C.LENGTH_UNSET) {
int adCount = adIndexInAdBreak + 1; adCounts[adGroupIndex] = adCountInAdGroup;
adBreakAds[adBreakIndex] = Arrays.copyOf(adBreakAds[adBreakIndex], adCount); maybeUpdateSourceInfo();
} }
adBreakAds[adBreakIndex][adIndexInAdBreak] = ad;
maybeUpdateSourceInfo();
} }
private void maybeUpdateSourceInfo() { private void maybeUpdateSourceInfo() {
if (adBreakTimesUs == null || contentTimeline == null) { if (adGroupTimesUs != null && contentTimeline != null) {
// We don't have enough information to start building the timeline yet. SinglePeriodAdTimeline timeline = new SinglePeriodAdTimeline(contentTimeline, adGroupTimesUs,
return; hasPlayedAdGroup, adCounts, isAdAvailable, adDurationsUs);
listener.onSourceInfoRefreshed(timeline, contentManifest);
} }
AdTimeline.Builder builder = new AdTimeline.Builder(contentTimeline);
int count = adBreakTimesUs.length;
boolean preroll = adBreakTimesUs[0] == 0;
boolean postroll = adBreakTimesUs[count - 1] == C.TIME_UNSET;
int midrollCount = count - (preroll ? 1 : 0) - (postroll ? 1 : 0);
int adBreakIndex = 0;
long contentTimeUs = 0;
if (preroll) {
addAdBreak(builder, adBreakIndex++);
}
for (int i = 0; i < midrollCount; i++) {
long startTimeUs = contentTimeUs;
contentTimeUs = adBreakTimesUs[adBreakIndex];
builder.addContent(startTimeUs, contentTimeUs);
addAdBreak(builder, adBreakIndex++);
}
builder.addContent(contentTimeUs, C.TIME_UNSET);
if (postroll) {
addAdBreak(builder, adBreakIndex);
}
timeline = builder.build();
listener.onSourceInfoRefreshed(timeline, contentManifest);
}
private void addAdBreak(AdTimeline.Builder builder, int adBreakIndex) {
int adCount = adBreakMediaSources[adBreakIndex].length;
AdPodInfo adPodInfo = null;
for (int adIndex = 0; adIndex < adCount; adIndex++) {
Timeline adTimeline = adBreakTimelines[adBreakIndex][adIndex];
long adDurationUs = adTimeline != null
? adTimeline.getPeriod(0, new Timeline.Period()).getDurationUs() : C.TIME_UNSET;
Ad ad = adIndex < adBreakAds[adBreakIndex].length
? adBreakAds[adBreakIndex][adIndex] : null;
builder.addAdPeriod(ad, adBreakIndex, adIndex, adDurationUs);
if (ad != null) {
adPodInfo = ad.getAdPodInfo();
}
}
if (adPodInfo == null || adPodInfo.getTotalAds() > adCount) {
// We don't know how many ads are in the ad break, or they have not loaded yet.
builder.addAdPeriod(null, adBreakIndex, adCount, C.TIME_UNSET);
}
}
private void onAdBreakPlayedToEnd(int adBreakIndex) {
playedAdBreak[adBreakIndex] = true;
} }
/** /**
@ -352,7 +276,7 @@ public final class ImaAdsMediaSource implements MediaSource {
ExtractorMediaSource.EventListener { ExtractorMediaSource.EventListener {
@Override @Override
public void onAdBreakTimesUsLoaded(final long[] adBreakTimesUs) { public void onAdGroupTimesUsLoaded(final long[] adGroupTimesUs) {
if (released) { if (released) {
return; return;
} }
@ -362,13 +286,13 @@ public final class ImaAdsMediaSource implements MediaSource {
if (released) { if (released) {
return; return;
} }
ImaAdsMediaSource.this.onAdBreakTimesUsLoaded(adBreakTimesUs); ImaAdsMediaSource.this.onAdGroupTimesUsLoaded(adGroupTimesUs);
} }
}); });
} }
@Override @Override
public void onUriLoaded(final int adBreakIndex, final int adIndexInAdBreak, final Uri uri) { public void onAdGroupPlayedToEnd(final int adGroupIndex) {
if (released) { if (released) {
return; return;
} }
@ -378,13 +302,13 @@ public final class ImaAdsMediaSource implements MediaSource {
if (released) { if (released) {
return; return;
} }
ImaAdsMediaSource.this.onAdUriLoaded(adBreakIndex, adIndexInAdBreak, uri); ImaAdsMediaSource.this.onAdGroupPlayedToEnd(adGroupIndex);
} }
}); });
} }
@Override @Override
public void onAdLoaded(final int adBreakIndex, final int adIndexInAdBreak, final Ad ad) { public void onAdUriLoaded(final int adGroupIndex, final int adIndexInAdGroup, final Uri uri) {
if (released) { if (released) {
return; return;
} }
@ -394,13 +318,13 @@ public final class ImaAdsMediaSource implements MediaSource {
if (released) { if (released) {
return; return;
} }
ImaAdsMediaSource.this.onAdLoaded(adBreakIndex, adIndexInAdBreak, ad); ImaAdsMediaSource.this.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, uri);
} }
}); });
} }
@Override @Override
public void onAdBreakPlayedToEnd(final int adBreakIndex) { public void onAdGroupLoaded(final int adGroupIndex, final int adCountInAdGroup) {
if (released) { if (released) {
return; return;
} }
@ -410,7 +334,7 @@ public final class ImaAdsMediaSource implements MediaSource {
if (released) { if (released) {
return; return;
} }
ImaAdsMediaSource.this.onAdBreakPlayedToEnd(adBreakIndex); ImaAdsMediaSource.this.onAdGroupLoaded(adGroupIndex, adCountInAdGroup);
} }
}); });
} }
@ -433,99 +357,4 @@ public final class ImaAdsMediaSource implements MediaSource {
} }
private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
private final int index;
private final Allocator allocator;
public MediaPeriod mediaPeriod;
private MediaPeriod.Callback callback;
private long positionUs;
public DeferredMediaPeriod(int index, Allocator allocator) {
this.index = index;
this.allocator = allocator;
}
public void setMediaSource(MediaSource mediaSource) {
mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(index), allocator);
if (callback != null) {
mediaPeriod.prepare(this, positionUs);
}
}
@Override
public void prepare(Callback callback, long positionUs) {
this.callback = callback;
this.positionUs = positionUs;
if (mediaPeriod != null) {
mediaPeriod.prepare(this, positionUs);
}
}
@Override
public void maybeThrowPrepareError() throws IOException {
if (mediaPeriod != null) {
mediaPeriod.maybeThrowPrepareError();
}
}
@Override
public TrackGroupArray getTrackGroups() {
return mediaPeriod.getTrackGroups();
}
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,
positionUs);
}
@Override
public void discardBuffer(long positionUs) {
// Do nothing.
}
@Override
public long readDiscontinuity() {
return mediaPeriod.readDiscontinuity();
}
@Override
public long getBufferedPositionUs() {
return mediaPeriod.getBufferedPositionUs();
}
@Override
public long seekToUs(long positionUs) {
return mediaPeriod.seekToUs(positionUs);
}
@Override
public long getNextLoadPositionUs() {
return mediaPeriod.getNextLoadPositionUs();
}
@Override
public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
}
// MediaPeriod.Callback implementation.
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
Assertions.checkArgument(this.mediaPeriod == mediaPeriod);
callback.onPrepared(this);
}
@Override
public void onContinueLoadingRequested(MediaPeriod mediaPeriod) {
Assertions.checkArgument(this.mediaPeriod == mediaPeriod);
callback.onContinueLoadingRequested(this);
}
}
} }

View File

@ -0,0 +1,92 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.ima;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Assertions;
/**
* A {@link Timeline} for sources that have ads.
*/
public final class SinglePeriodAdTimeline extends Timeline {
private final Timeline contentTimeline;
private final long[] adGroupTimesUs;
private final boolean[] hasPlayedAdGroup;
private final int[] adCounts;
private final boolean[][] isAdAvailable;
private final long[][] adDurationsUs;
/**
* Creates a new timeline with a single period containing the specified ads.
*
* @param contentTimeline The timeline of the content alongside which ads will be played. It must
* have one window and one period.
* @param adGroupTimesUs The times of ad groups relative to the start of the period, in
* microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that
* the period has a postroll ad.
* @param hasPlayedAdGroup Whether each ad group has been played.
* @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET}
* if the number of ads is not yet known.
* @param isAdAvailable Whether each ad in each ad group is available.
* @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element
* may be {@link C#TIME_UNSET} if the duration is not yet known.
*/
public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs,
boolean[] hasPlayedAdGroup, int[] adCounts, boolean[][] isAdAvailable,
long[][] adDurationsUs) {
Assertions.checkState(contentTimeline.getPeriodCount() == 1);
Assertions.checkState(contentTimeline.getWindowCount() == 1);
this.contentTimeline = contentTimeline;
this.adGroupTimesUs = adGroupTimesUs;
this.hasPlayedAdGroup = hasPlayedAdGroup;
this.adCounts = adCounts;
this.isAdAvailable = isAdAvailable;
this.adDurationsUs = adDurationsUs;
}
@Override
public int getWindowCount() {
return 1;
}
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
return contentTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
}
@Override
public int getPeriodCount() {
return 1;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
contentTimeline.getPeriod(periodIndex, period, setIds);
period.set(period.id, period.uid, period.windowIndex, period.durationUs,
period.getPositionInWindowUs(), adGroupTimesUs, hasPlayedAdGroup, adCounts,
isAdAvailable, adDurationsUs);
return period;
}
@Override
public int getIndexOfPeriod(Object uid) {
return contentTimeline.getIndexOfPeriod(uid);
}
}

View File

@ -483,7 +483,7 @@ public final class ExoPlayerTest extends TestCase {
public Period getPeriod(int periodIndex, Period period, boolean setIds) { public Period getPeriod(int periodIndex, Period period, boolean setIds) {
TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex]; TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex];
Object id = setIds ? periodIndex : null; Object id = setIds ? periodIndex : null;
return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false); return period.set(id, id, periodIndex, windowDefinition.durationUs, 0);
} }
@Override @Override

View File

@ -63,7 +63,7 @@ public class TimelineTest extends TestCase {
@Override @Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) { public Period getPeriod(int periodIndex, Period period, boolean setIds) {
return period.set(new int[] { id, periodIndex }, null, 0, WINDOW_DURATION_US, 0, false); return period.set(new int[] { id, periodIndex }, null, 0, WINDOW_DURATION_US, 0);
} }
@Override @Override

View File

@ -758,24 +758,24 @@ public final class C {
/** /**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving * Converts a time in microseconds to the corresponding time in milliseconds, preserving
* {@link #TIME_UNSET} values. * {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values.
* *
* @param timeUs The time in microseconds. * @param timeUs The time in microseconds.
* @return The corresponding time in milliseconds. * @return The corresponding time in milliseconds.
*/ */
public static long usToMs(long timeUs) { public static long usToMs(long timeUs) {
return timeUs == TIME_UNSET ? TIME_UNSET : (timeUs / 1000); return (timeUs == TIME_UNSET || timeUs == TIME_END_OF_SOURCE) ? timeUs : (timeUs / 1000);
} }
/** /**
* Converts a time in milliseconds to the corresponding time in microseconds, preserving * Converts a time in milliseconds to the corresponding time in microseconds, preserving
* {@link #TIME_UNSET} values. * {@link #TIME_UNSET} values and {@link #TIME_END_OF_SOURCE} values.
* *
* @param timeMs The time in milliseconds. * @param timeMs The time in milliseconds.
* @return The corresponding time in microseconds. * @return The corresponding time in microseconds.
*/ */
public static long msToUs(long timeMs) { public static long msToUs(long timeMs) {
return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000); return (timeMs == TIME_UNSET || timeMs == TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000);
} }
/** /**

View File

@ -264,13 +264,6 @@ public abstract class Timeline {
*/ */
public long durationUs; public long durationUs;
// TODO: Remove this flag now that in-period ads are supported.
/**
* Whether this period contains an ad.
*/
public boolean isAd;
private long positionInWindowUs; private long positionInWindowUs;
private long[] adGroupTimesUs; private long[] adGroupTimesUs;
private boolean[] hasPlayedAdGroup; private boolean[] hasPlayedAdGroup;
@ -289,12 +282,11 @@ public abstract class Timeline {
* @param positionInWindowUs The position of the start of this period relative to the start of * @param positionInWindowUs The position of the start of this period relative to the start of
* the window to which it belongs, in milliseconds. May be negative if the start of the * the window to which it belongs, in milliseconds. May be negative if the start of the
* period is not within the window. * period is not within the window.
* @param isAd Whether this period is an ad.
* @return This period, for convenience. * @return This period, for convenience.
*/ */
public Period set(Object id, Object uid, int windowIndex, long durationUs, public Period set(Object id, Object uid, int windowIndex, long durationUs,
long positionInWindowUs, boolean isAd) { long positionInWindowUs) {
return set(id, uid, windowIndex, durationUs, positionInWindowUs, isAd, null, null, null, null, return set(id, uid, windowIndex, durationUs, positionInWindowUs, null, null, null, null,
null); null);
} }
@ -309,7 +301,6 @@ public abstract class Timeline {
* @param positionInWindowUs The position of the start of this period relative to the start of * @param positionInWindowUs The position of the start of this period relative to the start of
* the window to which it belongs, in milliseconds. May be negative if the start of the * the window to which it belongs, in milliseconds. May be negative if the start of the
* period is not within the window. * period is not within the window.
* @param isAd Whether this period is an ad.
* @param adGroupTimesUs The times of ad groups relative to the start of the period, in * @param adGroupTimesUs The times of ad groups relative to the start of the period, in
* microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that
* the period has a postroll ad. * the period has a postroll ad.
@ -322,14 +313,13 @@ public abstract class Timeline {
* @return This period, for convenience. * @return This period, for convenience.
*/ */
public Period set(Object id, Object uid, int windowIndex, long durationUs, public Period set(Object id, Object uid, int windowIndex, long durationUs,
long positionInWindowUs, boolean isAd, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup, long positionInWindowUs, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup, int[] adCounts,
int[] adCounts, boolean[][] isAdAvailable, long[][] adDurationsUs) { boolean[][] isAdAvailable, long[][] adDurationsUs) {
this.id = id; this.id = id;
this.uid = uid; this.uid = uid;
this.windowIndex = windowIndex; this.windowIndex = windowIndex;
this.durationUs = durationUs; this.durationUs = durationUs;
this.positionInWindowUs = positionInWindowUs; this.positionInWindowUs = positionInWindowUs;
this.isAd = isAd;
this.adGroupTimesUs = adGroupTimesUs; this.adGroupTimesUs = adGroupTimesUs;
this.hasPlayedAdGroup = hasPlayedAdGroup; this.hasPlayedAdGroup = hasPlayedAdGroup;
this.adCounts = adCounts; this.adCounts = adCounts;

View File

@ -99,7 +99,7 @@ public final class SinglePeriodTimeline extends Timeline {
public Period getPeriod(int periodIndex, Period period, boolean setIds) { public Period getPeriod(int periodIndex, Period period, boolean setIds) {
Assertions.checkIndex(periodIndex, 0, 1); Assertions.checkIndex(periodIndex, 0, 1);
Object id = setIds ? ID : null; Object id = setIds ? ID : null;
return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false); return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs);
} }
@Override @Override

View File

@ -653,7 +653,7 @@ public final class DashMediaSource implements MediaSource {
+ Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null; + Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null;
return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex), return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex),
C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs) C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs)
- offsetInFirstPeriodUs, false); - offsetInFirstPeriodUs);
} }
@Override @Override

View File

@ -78,13 +78,13 @@ public interface TimeBar {
void setDuration(long duration); void setDuration(long duration);
/** /**
* Sets the times of ad breaks. * Sets the times of ad groups.
* *
* @param adBreakTimesMs An array where the first {@code adBreakCount} elements are the times of * @param adGroupTimesMs An array where the first {@code adGroupCount} elements are the times of
* ad breaks in milliseconds. May be {@code null} if there are no ad breaks. * ad groups in milliseconds. May be {@code null} if there are no ad groups.
* @param adBreakCount The number of ad breaks. * @param adGroupCount The number of ad groups.
*/ */
void setAdGroupTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount); void setAdGroupTimesMs(@Nullable long[] adGroupTimesMs, int adGroupCount);
/** /**
* Listener for scrubbing events. * Listener for scrubbing events.