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:
* 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
sometimes delayed, meaning that midroll ads take a long time to start and the
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 {
/**
* 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.
*
* @param adBreakIndex The index of the ad break containing the ad with the media URI.
* @param adIndexInAdBreak The index of the ad in its ad break.
* @param adGroupIndex The index of the ad group containing the ad with the media URI.
* @param adIndexInAdGroup The index of the ad in its ad group.
* @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 adIndexInAdBreak The index of the ad in its ad break.
* @param ad The {@link Ad} instance for the ad.
* @param adGroupIndex The index of the ad group containing the ad.
* @param adCountInAdGroup The number of ads in the ad group.
*/
void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad);
/**
* Called when the specified ad break has been played to the end.
*
* @param adBreakIndex The index of the ad break.
*/
void onAdBreakPlayedToEnd(int adBreakIndex);
void onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup);
/**
* Called when there was an error loading ads.
@ -127,51 +126,45 @@ import java.util.List;
private final AdsLoader adsLoader;
private AdsManager adsManager;
private AdTimeline adTimeline;
private long[] adGroupTimesUs;
private int[] adsLoadedInAdGroup;
private Timeline timeline;
private long contentDurationMs;
private int lastContentPeriodIndex;
private int playerPeriodIndex;
private boolean released;
// 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)}.
*/
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
* If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #playAd()} and not
* {@link #stopAd()}.
*/
private boolean playingAd;
/**
* If {@link #isAdDisplayed} is set, stores whether IMA has called {@link #pauseAd()} since a
* preceding call to {@link #playAd()} for the current ad.
* If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #pauseAd()} since
* a preceding call to {@link #playAd()} for the current ad.
*/
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
* {@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.
*/
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.
@ -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 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.
* @param player The player instance that will play the loaded ad schedule. The player's timeline
* must be an {@link AdTimeline} matching the loaded ad schedule.
* @param player The player instance that will play the loaded ad schedule.
* @param eventListener Listener for ad loader events.
*/
public ImaAdsLoader(Context context, Uri adTagUri, ViewGroup adUiViewGroup,
@ -201,9 +201,10 @@ import java.util.List;
period = new Timeline.Period();
adCallbacks = new ArrayList<>(1);
lastContentPeriodIndex = C.INDEX_UNSET;
adCountInAdBreak = C.INDEX_UNSET;
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
pendingContentPositionMs = C.TIME_UNSET;
adGroupIndex = C.INDEX_UNSET;
contentDurationMs = C.TIME_UNSET;
player.addListener(this);
@ -262,13 +263,16 @@ import java.util.List;
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.
@Override
public void onAdEvent(AdEvent adEvent) {
Ad ad = adEvent.getAd();
if (DEBUG) {
Log.d(TAG, "onAdEvent " + adEvent.getType());
}
@ -278,20 +282,18 @@ import java.util.List;
}
switch (adEvent.getType()) {
case LOADED:
adsManager.start();
break;
case STARTED:
// Note: This event is sometimes delivered several seconds after playAd is called.
// See [Internal: b/37775441].
Ad ad = adEvent.getAd();
// The ad position is not always accurate when using preloading. See [Internal: b/62613240].
AdPodInfo adPodInfo = ad.getAdPodInfo();
adCountInAdBreak = adPodInfo.getTotalAds();
int podIndex = adPodInfo.getPodIndex();
adGroupIndex = podIndex == -1 ? adGroupTimesUs.length - 1 : podIndex;
int adPosition = adPodInfo.getAdPosition();
eventListener.onAdLoaded(adBreakIndex, adPosition - 1, ad);
int adCountInAdGroup = adPodInfo.getTotalAds();
adsManager.start();
if (DEBUG) {
Log.d(TAG, "Started ad " + adPosition + " of " + adCountInAdBreak + " in ad break "
+ adBreakIndex);
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group "
+ adGroupIndex);
}
eventListener.onAdGroupLoaded(adGroupIndex, adCountInAdGroup);
break;
case CONTENT_PAUSE_REQUESTED:
// After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
@ -325,40 +327,41 @@ import java.util.List;
@Override
public VideoProgressUpdate getContentProgress() {
if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long contentEndTimeMs = C.usToMs(adTimeline.getContentEndTimeUs(imaPeriodIndex));
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
return new VideoProgressUpdate(contentEndTimeMs + elapsedSinceEndMs, contentDurationMs);
if (pendingContentPositionMs != C.TIME_UNSET) {
sentPendingContentPositionMs = true;
return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs);
}
if (adTimeline == null || isAdDisplayed || imaPeriodIndex != playerPeriodIndex
|| contentDurationMs == C.TIME_UNSET) {
if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long adGroupTimeMs = C.usToMs(adGroupTimesUs[adGroupIndex]);
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;
}
checkForContentComplete();
long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex))
+ player.getCurrentPosition();
return new VideoProgressUpdate(positionMs, contentDurationMs);
return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs);
}
// VideoAdPlayer implementation.
@Override
public VideoProgressUpdate getAdProgress() {
if (adTimeline == null || !isAdDisplayed || imaPeriodIndex != playerPeriodIndex
|| adTimeline.getPeriod(imaPeriodIndex, period).getDurationUs() == C.TIME_UNSET) {
if (!player.isPlayingAd()) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
}
return new VideoProgressUpdate(player.getCurrentPosition(), period.getDurationMs());
return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration());
}
@Override
public void loadAd(String adUriString) {
int adIndexInAdGroup = adsLoadedInAdGroup[adGroupIndex]++;
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));
adIndexInAdBreak++;
eventListener.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString));
}
@Override
@ -376,7 +379,6 @@ import java.util.List;
if (DEBUG) {
Log.d(TAG, "playAd");
}
Assertions.checkState(isAdDisplayed);
if (playingAd && !pausedInAd) {
// Work around an issue where IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
@ -443,18 +445,12 @@ import java.util.List;
// The player is being re-prepared and this source will be released.
return;
}
if (adTimeline == null) {
// TODO: Handle initial seeks after the first period.
isAdDisplayed = timeline.getPeriod(0, period).isAd;
imaPeriodIndex = 0;
player.seekTo(0, 0);
}
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--;
Assertions.checkArgument(timeline.getPeriodCount() == 1);
this.timeline = timeline;
contentDurationMs = C.usToMs(timeline.getPeriod(0, period).durationUs);
if (player.isPlayingAd()) {
playingAdGroupIndex = player.getCurrentAdGroupIndex();
playingAdIndexInAdGroup = player.getCurrentAdIndexInAdGroup();
}
}
@ -470,9 +466,9 @@ import java.util.List;
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) {
if (!playingAd && playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) {
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.
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
for (VideoAdPlayerCallback callback : adCallbacks) {
@ -488,7 +484,7 @@ import java.util.List;
@Override
public void onPlayerError(ExoPlaybackException error) {
if (isAdDisplayed && adTimeline.isPeriodAd(playerPeriodIndex)) {
if (player.isPlayingAd()) {
for (VideoAdPlayerCallback callback : adCallbacks) {
callback.onError();
}
@ -497,23 +493,41 @@ import java.util.List;
@Override
public void onPositionDiscontinuity() {
if (player.getCurrentPeriodIndex() == playerPeriodIndex + 1) {
if (isAdDisplayed) {
// 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();
}
} else {
player.setPlayWhenReady(false);
if (imaPeriodIndex == playerPeriodIndex) {
// IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position.
Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET);
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
if (!player.isPlayingAd() && playingAdGroupIndex == C.INDEX_UNSET) {
long positionUs = C.msToUs(player.getCurrentPosition());
int adGroupIndex = timeline.getPeriod(0, period).getAdGroupIndexForPositionUs(positionUs);
if (adGroupIndex != C.INDEX_UNSET) {
sentPendingContentPositionMs = false;
pendingContentPositionMs = player.getCurrentPosition();
}
return;
}
boolean adFinished = (!player.isPlayingAd() && playingAdGroupIndex != C.INDEX_UNSET)
|| (player.isPlayingAd() && playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup());
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
@ -527,70 +541,42 @@ import java.util.List;
* Resumes the player, ensuring the current period is a content period by seeking if necessary.
*/
private void resumeContentInternal() {
if (adTimeline != null) {
if (imaPeriodIndex < lastContentPeriodIndex) {
if (playingAd) {
// Work around an issue where IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
if (DEBUG) {
Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
}
stopAdInternal();
if (contentDurationMs != C.TIME_UNSET) {
if (playingAd) {
// Work around an issue where IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
if (DEBUG) {
Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
}
while (adTimeline.isPeriodAd(imaPeriodIndex)) {
imaPeriodIndex++;
}
synchronizePlayerToIma();
stopAdInternal();
}
}
player.setPlayWhenReady(true);
clearFlags();
}
/**
* Pauses the player, and ensures that the current period is an ad period by seeking if necessary.
*/
private void pauseContentInternal() {
if (sentPendingContentPositionMs) {
pendingContentPositionMs = C.TIME_UNSET;
sentPendingContentPositionMs = false;
}
// IMA is requesting to pause content, so stop faking the content position.
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);
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() {
Assertions.checkState(playingAd);
if (imaPeriodIndex != adTimeline.getPeriodCount() - 1) {
player.setPlayWhenReady(false);
imaPeriodIndex++;
if (!adTimeline.isPeriodAd(imaPeriodIndex)) {
eventListener.onAdBreakPlayedToEnd(adBreakIndex);
adBreakIndex++;
adIndexInAdBreak = 0;
}
synchronizePlayerToIma();
} else {
eventListener.onAdBreakPlayedToEnd(adTimeline.getAdBreakIndex(imaPeriodIndex));
player.setPlayWhenReady(false);
if (!player.isPlayingAd()) {
eventListener.onAdGroupPlayedToEnd(adGroupIndex);
adGroupIndex = C.INDEX_UNSET;
}
clearFlags();
}
private void synchronizePlayerToIma() {
if (playerPeriodIndex != imaPeriodIndex) {
player.seekTo(imaPeriodIndex, 0);
}
isAdDisplayed = adTimeline.isPeriodAd(imaPeriodIndex);
private void clearFlags() {
// If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until
// the content is resumed.
playingAd = false;
@ -598,14 +584,9 @@ import java.util.List;
}
private void checkForContentComplete() {
if (adTimeline == null || isAdDisplayed || sentContentComplete) {
return;
}
long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex))
+ player.getCurrentPosition();
if (playerPeriodIndex == lastContentPeriodIndex
&& positionMs + END_OF_CONTENT_POSITION_THRESHOLD_MS
>= C.usToMs(adTimeline.getContentEndTimeUs(playerPeriodIndex))) {
if (contentDurationMs != C.TIME_UNSET
&& player.getCurrentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
&& !sentContentComplete) {
adsLoader.contentComplete();
if (DEBUG) {
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 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};
}
int count = cuePoints.size();
long[] adBreakTimesUs = new long[count];
long[] adGroupTimesUs = new long[count];
for (int i = 0; i < count; 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.Looper;
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.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
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.MediaPeriod;
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.DataSource;
import com.google.android.exoplayer2.util.Assertions;
@ -56,7 +50,8 @@ public final class ImaAdsMediaSource implements MediaSource {
private final ImaSdkSettings imaSdkSettings;
private final Handler mainHandler;
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 ExoPlayer player;
@ -65,13 +60,12 @@ public final class ImaAdsMediaSource implements MediaSource {
// Accessed on the player thread.
private Timeline contentTimeline;
private Object contentManifest;
private long[] adBreakTimesUs;
private boolean[] playedAdBreak;
private Ad[][] adBreakAds;
private Timeline[][] adBreakTimelines;
private MediaSource[][] adBreakMediaSources;
private DeferredMediaPeriod[][] adBreakDeferredMediaPeriods;
private AdTimeline timeline;
private long[] adGroupTimesUs;
private boolean[] hasPlayedAdGroup;
private int[] adCounts;
private MediaSource[][] adGroupMediaSources;
private boolean[][] isAdAvailable;
private long[][] adDurationsUs;
private MediaSource.Listener listener;
private IOException adLoadError;
@ -120,8 +114,11 @@ public final class ImaAdsMediaSource implements MediaSource {
this.imaSdkSettings = imaSdkSettings;
mainHandler = new Handler(Looper.getMainLooper());
adLoaderListener = new AdListener();
mediaSourceByMediaPeriod = new HashMap<>();
adBreakMediaSources = new MediaSource[0][];
adMediaSourceByMediaPeriod = new HashMap<>();
period = new Timeline.Period();
adGroupMediaSources = new MediaSource[0][];
isAdAvailable = new boolean[0][];
adDurationsUs = new long[0][];
}
@Override
@ -151,7 +148,7 @@ public final class ImaAdsMediaSource implements MediaSource {
throw adLoadError;
}
contentMediaSource.maybeThrowSourceInfoRefreshError();
for (MediaSource[] mediaSources : adBreakMediaSources) {
for (MediaSource[] mediaSources : adGroupMediaSources) {
for (MediaSource mediaSource : mediaSources) {
mediaSource.maybeThrowSourceInfoRefreshError();
}
@ -160,49 +157,23 @@ public final class ImaAdsMediaSource implements MediaSource {
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
int index = id.periodIndex;
if (timeline.isPeriodAd(index)) {
int adBreakIndex = timeline.getAdBreakIndex(index);
int adIndexInAdBreak = timeline.getAdIndexInAdBreak(index);
if (adIndexInAdBreak >= adBreakMediaSources[adBreakIndex].length) {
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;
if (id.isAd()) {
MediaSource mediaSource = adGroupMediaSources[id.adGroupIndex][id.adIndexInAdGroup];
MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator);
adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource);
return mediaPeriod;
} else {
long startUs = timeline.getContentStartTimeUs(index);
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;
return contentMediaSource.createPeriod(id, allocator);
}
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
if (mediaPeriod instanceof DeferredMediaPeriod) {
mediaPeriod = ((DeferredMediaPeriod) mediaPeriod).mediaPeriod;
if (mediaPeriod == null) {
// Nothing to do.
return;
}
} else if (mediaPeriod instanceof ClippingMediaPeriod) {
mediaPeriod = ((ClippingMediaPeriod) mediaPeriod).mediaPeriod;
if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) {
adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod);
} else {
contentMediaSource.releasePeriod(mediaPeriod);
}
mediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod);
}
@Override
@ -210,7 +181,7 @@ public final class ImaAdsMediaSource implements MediaSource {
released = true;
adLoadError = null;
contentMediaSource.releaseSource();
for (MediaSource[] mediaSources : adBreakMediaSources) {
for (MediaSource[] mediaSources : adGroupMediaSources) {
for (MediaSource mediaSource : mediaSources) {
mediaSource.releaseSource();
}
@ -229,19 +200,19 @@ public final class ImaAdsMediaSource implements MediaSource {
// Internal methods.
private void onAdBreakTimesUsLoaded(long[] adBreakTimesUs) {
Assertions.checkState(this.adBreakTimesUs == null);
this.adBreakTimesUs = adBreakTimesUs;
int adBreakCount = adBreakTimesUs.length;
adBreakAds = new Ad[adBreakCount][];
Arrays.fill(adBreakAds, new Ad[0]);
adBreakTimelines = new Timeline[adBreakCount][];
Arrays.fill(adBreakTimelines, new Timeline[0]);
adBreakMediaSources = new MediaSource[adBreakCount][];
Arrays.fill(adBreakMediaSources, new MediaSource[0]);
adBreakDeferredMediaPeriods = new DeferredMediaPeriod[adBreakCount][];
Arrays.fill(adBreakDeferredMediaPeriods, new DeferredMediaPeriod[0]);
playedAdBreak = new boolean[adBreakCount];
private void onAdGroupTimesUsLoaded(long[] adGroupTimesUs) {
Assertions.checkState(this.adGroupTimesUs == null);
int adGroupCount = adGroupTimesUs.length;
this.adGroupTimesUs = adGroupTimesUs;
hasPlayedAdGroup = new boolean[adGroupCount];
adCounts = new int[adGroupCount];
Arrays.fill(adCounts, C.LENGTH_UNSET);
adGroupMediaSources = new MediaSource[adGroupCount][];
Arrays.fill(adGroupMediaSources, new MediaSource[0]);
isAdAvailable = new boolean[adGroupCount][];
Arrays.fill(isAdAvailable, new boolean[0]);
adDurationsUs = new long[adGroupCount][];
Arrays.fill(adDurationsUs, new long[0]);
maybeUpdateSourceInfo();
}
@ -251,98 +222,51 @@ public final class ImaAdsMediaSource implements MediaSource {
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,
new DefaultExtractorsFactory(), mainHandler, adLoaderListener);
if (adBreakMediaSources[adBreakIndex].length <= adIndexInAdBreak) {
int adCount = adIndexInAdBreak + 1;
adBreakMediaSources[adBreakIndex] = Arrays.copyOf(adBreakMediaSources[adBreakIndex], adCount);
adBreakTimelines[adBreakIndex] = Arrays.copyOf(adBreakTimelines[adBreakIndex], adCount);
}
adBreakMediaSources[adBreakIndex][adIndexInAdBreak] = adMediaSource;
if (adIndexInAdBreak < adBreakDeferredMediaPeriods[adBreakIndex].length
&& adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak] != null) {
adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].setMediaSource(
adBreakMediaSources[adBreakIndex][adIndexInAdBreak]);
mediaSourceByMediaPeriod.put(
adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].mediaPeriod, adMediaSource);
int oldAdCount = adGroupMediaSources[adGroupIndex].length;
if (adIndexInAdGroup >= oldAdCount) {
int adCount = adIndexInAdGroup + 1;
adGroupMediaSources[adGroupIndex] = Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount);
isAdAvailable[adGroupIndex] = Arrays.copyOf(isAdAvailable[adGroupIndex], adCount);
adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount);
Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET);
}
adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource;
isAdAvailable[adGroupIndex][adIndexInAdGroup] = true;
adMediaSource.prepareSource(player, false, new Listener() {
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
onAdSourceInfoRefreshed(adBreakIndex, adIndexInAdBreak, timeline);
onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline);
}
});
}
private void onAdSourceInfoRefreshed(int adBreakIndex, int adIndexInAdBreak, Timeline timeline) {
adBreakTimelines[adBreakIndex][adIndexInAdBreak] = timeline;
private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) {
Assertions.checkArgument(timeline.getPeriodCount() == 1);
adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs();
maybeUpdateSourceInfo();
}
private void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad) {
if (adBreakAds[adBreakIndex].length <= adIndexInAdBreak) {
int adCount = adIndexInAdBreak + 1;
adBreakAds[adBreakIndex] = Arrays.copyOf(adBreakAds[adBreakIndex], adCount);
private void onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup) {
if (adCounts[adGroupIndex] == C.LENGTH_UNSET) {
adCounts[adGroupIndex] = adCountInAdGroup;
maybeUpdateSourceInfo();
}
adBreakAds[adBreakIndex][adIndexInAdBreak] = ad;
maybeUpdateSourceInfo();
}
private void maybeUpdateSourceInfo() {
if (adBreakTimesUs == null || contentTimeline == null) {
// We don't have enough information to start building the timeline yet.
return;
if (adGroupTimesUs != null && contentTimeline != null) {
SinglePeriodAdTimeline timeline = new SinglePeriodAdTimeline(contentTimeline, adGroupTimesUs,
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 {
@Override
public void onAdBreakTimesUsLoaded(final long[] adBreakTimesUs) {
public void onAdGroupTimesUsLoaded(final long[] adGroupTimesUs) {
if (released) {
return;
}
@ -362,13 +286,13 @@ public final class ImaAdsMediaSource implements MediaSource {
if (released) {
return;
}
ImaAdsMediaSource.this.onAdBreakTimesUsLoaded(adBreakTimesUs);
ImaAdsMediaSource.this.onAdGroupTimesUsLoaded(adGroupTimesUs);
}
});
}
@Override
public void onUriLoaded(final int adBreakIndex, final int adIndexInAdBreak, final Uri uri) {
public void onAdGroupPlayedToEnd(final int adGroupIndex) {
if (released) {
return;
}
@ -378,13 +302,13 @@ public final class ImaAdsMediaSource implements MediaSource {
if (released) {
return;
}
ImaAdsMediaSource.this.onAdUriLoaded(adBreakIndex, adIndexInAdBreak, uri);
ImaAdsMediaSource.this.onAdGroupPlayedToEnd(adGroupIndex);
}
});
}
@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) {
return;
}
@ -394,13 +318,13 @@ public final class ImaAdsMediaSource implements MediaSource {
if (released) {
return;
}
ImaAdsMediaSource.this.onAdLoaded(adBreakIndex, adIndexInAdBreak, ad);
ImaAdsMediaSource.this.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, uri);
}
});
}
@Override
public void onAdBreakPlayedToEnd(final int adBreakIndex) {
public void onAdGroupLoaded(final int adGroupIndex, final int adCountInAdGroup) {
if (released) {
return;
}
@ -410,7 +334,7 @@ public final class ImaAdsMediaSource implements MediaSource {
if (released) {
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) {
TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex];
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

View File

@ -63,7 +63,7 @@ public class TimelineTest extends TestCase {
@Override
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

View File

@ -758,24 +758,24 @@ public final class C {
/**
* 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.
* @return The corresponding time in milliseconds.
*/
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
* {@link #TIME_UNSET} values.
* {@link #TIME_UNSET} values and {@link #TIME_END_OF_SOURCE} values.
*
* @param timeMs The time in milliseconds.
* @return The corresponding time in microseconds.
*/
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;
// 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[] adGroupTimesUs;
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
* the window to which it belongs, in milliseconds. May be negative if the start of the
* period is not within the window.
* @param isAd Whether this period is an ad.
* @return This period, for convenience.
*/
public Period set(Object id, Object uid, int windowIndex, long durationUs,
long positionInWindowUs, boolean isAd) {
return set(id, uid, windowIndex, durationUs, positionInWindowUs, isAd, null, null, null, null,
long positionInWindowUs) {
return set(id, uid, windowIndex, durationUs, positionInWindowUs, 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
* the window to which it belongs, in milliseconds. May be negative if the start of the
* 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
* microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that
* the period has a postroll ad.
@ -322,14 +313,13 @@ public abstract class Timeline {
* @return This period, for convenience.
*/
public Period set(Object id, Object uid, int windowIndex, long durationUs,
long positionInWindowUs, boolean isAd, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup,
int[] adCounts, boolean[][] isAdAvailable, long[][] adDurationsUs) {
long positionInWindowUs, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup, int[] adCounts,
boolean[][] isAdAvailable, long[][] adDurationsUs) {
this.id = id;
this.uid = uid;
this.windowIndex = windowIndex;
this.durationUs = durationUs;
this.positionInWindowUs = positionInWindowUs;
this.isAd = isAd;
this.adGroupTimesUs = adGroupTimesUs;
this.hasPlayedAdGroup = hasPlayedAdGroup;
this.adCounts = adCounts;

View File

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

View File

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

View File

@ -78,13 +78,13 @@ public interface TimeBar {
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
* ad breaks in milliseconds. May be {@code null} if there are no ad breaks.
* @param adBreakCount The number of ad breaks.
* @param adGroupTimesMs An array where the first {@code adGroupCount} elements are the times of
* ad groups in milliseconds. May be {@code null} if there are no ad groups.
* @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.