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:
parent
6509dce6b7
commit
1f815db367
@ -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.
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user