Commit 1a5b304b by Oliver Woodman Committed by GitHub

Merge pull request #7517 from google/dev-v2-r2.11.6

r2.11.6
parents 6026b919 67c99e1d
# Release notes # # Release notes #
### 2.11.6 (2020-06-24) ###
* UI: Prevent `PlayerView` from temporarily hiding the video surface when
seeking to an unprepared period within the current window. For example when
seeking over an ad group, or to the next period in a multi-period DASH
stream ([#5507](https://github.com/google/ExoPlayer/issues/5507)).
* IMA extension:
* Add option to skip ads before the start position.
* Catch unexpected errors in `stopAd` to avoid a crash
([#7492](https://github.com/google/ExoPlayer/issues/7492)).
* Fix a bug that caused playback to be stuck buffering on resuming from
the background after all ads had played to the end
([#7508](https://github.com/google/ExoPlayer/issues/7508)).
* Fix a bug where the number of ads in an ad group couldn't change
([#7477](https://github.com/google/ExoPlayer/issues/7477)).
* Work around unexpected `pauseAd`/`stopAd` for ads that have preloaded
on seeking to another position
([#7492](https://github.com/google/ExoPlayer/issues/7492)).
* Fix incorrect rounding of ad cue points.
* Fix handling of postrolls preloading
([#7518](https://github.com/google/ExoPlayer/issues/7518)).
### 2.11.5 (2020-06-05) ### ### 2.11.5 (2020-06-05) ###
* Improve the smoothness of video playback immediately after starting, seeking * Improve the smoothness of video playback immediately after starting, seeking
...@@ -16,10 +38,10 @@ ...@@ -16,10 +38,10 @@
([#7306](https://github.com/google/ExoPlayer/issues/7306)). ([#7306](https://github.com/google/ExoPlayer/issues/7306)).
* Fix issue in `AudioTrackPositionTracker` that could cause negative positions * Fix issue in `AudioTrackPositionTracker` that could cause negative positions
to be reported at the start of playback and immediately after seeking to be reported at the start of playback and immediately after seeking
([#7456](https://github.com/google/ExoPlayer/issues/7456). ([#7456](https://github.com/google/ExoPlayer/issues/7456)).
* Fix further cases where downloads would sometimes not resume after their * Fix further cases where downloads would sometimes not resume after their
network requirements are met network requirements are met
([#7453](https://github.com/google/ExoPlayer/issues/7453). ([#7453](https://github.com/google/ExoPlayer/issues/7453)).
* DASH: * DASH:
* Merge trick play adaptation sets (i.e., adaptation sets marked with * Merge trick play adaptation sets (i.e., adaptation sets marked with
`http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as
...@@ -99,10 +121,10 @@ ...@@ -99,10 +121,10 @@
`DefaultAudioSink` constructor `DefaultAudioSink` constructor
([#7134](https://github.com/google/ExoPlayer/issues/7134)). ([#7134](https://github.com/google/ExoPlayer/issues/7134)).
* Workaround issue that could cause slower than realtime playback of AAC on * Workaround issue that could cause slower than realtime playback of AAC on
Android 10 ([#6671](https://github.com/google/ExoPlayer/issues/6671). Android 10 ([#6671](https://github.com/google/ExoPlayer/issues/6671)).
* Fix case where another app spuriously holding transient audio focus could * Fix case where another app spuriously holding transient audio focus could
prevent ExoPlayer from acquiring audio focus for an indefinite period of prevent ExoPlayer from acquiring audio focus for an indefinite period of
time ([#7182](https://github.com/google/ExoPlayer/issues/7182). time ([#7182](https://github.com/google/ExoPlayer/issues/7182)).
* Fix case where the player volume could be permanently ducked if audio focus * Fix case where the player volume could be permanently ducked if audio focus
was released whilst ducking. was released whilst ducking.
* Fix playback of WAV files with trailing non-media bytes * Fix playback of WAV files with trailing non-media bytes
...@@ -1022,7 +1044,7 @@ ...@@ -1022,7 +1044,7 @@
([#4492](https://github.com/google/ExoPlayer/issues/4492) and ([#4492](https://github.com/google/ExoPlayer/issues/4492) and
[#4634](https://github.com/google/ExoPlayer/issues/4634)). [#4634](https://github.com/google/ExoPlayer/issues/4634)).
* Fix issue where removing looping media from a playlist throws an exception * Fix issue where removing looping media from a playlist throws an exception
([#4871](https://github.com/google/ExoPlayer/issues/4871). ([#4871](https://github.com/google/ExoPlayer/issues/4871)).
* Fix issue where the preferred audio or text track would not be selected if * Fix issue where the preferred audio or text track would not be selected if
mapped onto a secondary renderer of the corresponding type mapped onto a secondary renderer of the corresponding type
([#4711](http://github.com/google/ExoPlayer/issues/4711)). ([#4711](http://github.com/google/ExoPlayer/issues/4711)).
...@@ -1439,7 +1461,7 @@ ...@@ -1439,7 +1461,7 @@
resources when the playback thread has quit by the time the loading task has resources when the playback thread has quit by the time the loading task has
completed. completed.
* ID3: Better handle malformed ID3 data * ID3: Better handle malformed ID3 data
([#3792](https://github.com/google/ExoPlayer/issues/3792). ([#3792](https://github.com/google/ExoPlayer/issues/3792)).
* Support 14-bit mode and little endianness in DTS PES packets * Support 14-bit mode and little endianness in DTS PES packets
([#3340](https://github.com/google/ExoPlayer/issues/3340)). ([#3340](https://github.com/google/ExoPlayer/issues/3340)).
* Demo app: Add ability to download not DRM protected content. * Demo app: Add ability to download not DRM protected content.
......
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.11.5' releaseVersion = '2.11.6'
releaseVersionCode = 2011005 releaseVersionCode = 2011006
minSdkVersion = 16 minSdkVersion = 16
appTargetSdkVersion = 29 appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved
......
...@@ -476,6 +476,11 @@ ...@@ -476,6 +476,11 @@
"name": "VMAP full, empty, full midrolls", "name": "VMAP full, empty, full midrolls",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2" "ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2"
},
{
"name": "VMAP midroll at 1765 s",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
"ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-large"
} }
] ]
}, },
......
/*
* Copyright (C) 2020 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.source.ads.AdPlaybackState;
import java.util.Arrays;
import java.util.List;
/**
* Static utility class for constructing {@link AdPlaybackState} instances from IMA-specific data.
*/
/* package */ final class AdPlaybackStateFactory {
private AdPlaybackStateFactory() {}
/**
* Construct an {@link AdPlaybackState} from the provided {@code cuePoints}.
*
* @param cuePoints The cue points of the ads in seconds.
* @return The {@link AdPlaybackState}.
*/
public static AdPlaybackState fromCuePoints(List<Float> cuePoints) {
if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad.
return new AdPlaybackState(/* adGroupTimesUs...= */ 0);
}
int count = cuePoints.size();
long[] adGroupTimesUs = new long[count];
int adGroupIndex = 0;
for (int i = 0; i < count; i++) {
double cuePoint = cuePoints.get(i);
if (cuePoint == -1.0) {
adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE;
} else {
adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint);
}
}
// Cue points may be out of order, so sort them.
Arrays.sort(adGroupTimesUs, 0, adGroupIndex);
return new AdPlaybackState(adGroupTimesUs);
}
}
...@@ -86,14 +86,7 @@ import java.util.Set; ...@@ -86,14 +86,7 @@ import java.util.Set;
* For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} and {@link * For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} and {@link
* AdViewProvider#getAdOverlayViews()}. * AdViewProvider#getAdOverlayViews()}.
*/ */
public final class ImaAdsLoader public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
implements Player.EventListener,
AdsLoader,
VideoAdPlayer,
ContentProgressProvider,
AdErrorListener,
AdsLoadedListener,
AdEventListener {
static { static {
ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
...@@ -123,6 +116,7 @@ public final class ImaAdsLoader ...@@ -123,6 +116,7 @@ public final class ImaAdsLoader
private int mediaLoadTimeoutMs; private int mediaLoadTimeoutMs;
private int mediaBitrate; private int mediaBitrate;
private boolean focusSkipButtonWhenAvailable; private boolean focusSkipButtonWhenAvailable;
private boolean playAdBeforeStartPosition;
private ImaFactory imaFactory; private ImaFactory imaFactory;
/** /**
...@@ -137,6 +131,7 @@ public final class ImaAdsLoader ...@@ -137,6 +131,7 @@ public final class ImaAdsLoader
mediaLoadTimeoutMs = TIMEOUT_UNSET; mediaLoadTimeoutMs = TIMEOUT_UNSET;
mediaBitrate = BITRATE_UNSET; mediaBitrate = BITRATE_UNSET;
focusSkipButtonWhenAvailable = true; focusSkipButtonWhenAvailable = true;
playAdBeforeStartPosition = true;
imaFactory = new DefaultImaFactory(); imaFactory = new DefaultImaFactory();
} }
...@@ -250,6 +245,21 @@ public final class ImaAdsLoader ...@@ -250,6 +245,21 @@ public final class ImaAdsLoader
return this; return this;
} }
/**
* Sets whether to play an ad before the start position when beginning playback. If {@code
* true}, an ad will be played if there is one at or before the start position. If {@code
* false}, an ad will be played only if there is one exactly at the start position. The default
* setting is {@code true}.
*
* @param playAdBeforeStartPosition Whether to play an ad before the start position when
* beginning playback.
* @return This builder, for convenience.
*/
public Builder setPlayAdBeforeStartPosition(boolean playAdBeforeStartPosition) {
this.playAdBeforeStartPosition = playAdBeforeStartPosition;
return this;
}
@VisibleForTesting @VisibleForTesting
/* package */ Builder setImaFactory(ImaFactory imaFactory) { /* package */ Builder setImaFactory(ImaFactory imaFactory) {
this.imaFactory = Assertions.checkNotNull(imaFactory); this.imaFactory = Assertions.checkNotNull(imaFactory);
...@@ -275,6 +285,7 @@ public final class ImaAdsLoader ...@@ -275,6 +285,7 @@ public final class ImaAdsLoader
mediaLoadTimeoutMs, mediaLoadTimeoutMs,
mediaBitrate, mediaBitrate,
focusSkipButtonWhenAvailable, focusSkipButtonWhenAvailable,
playAdBeforeStartPosition,
adUiElements, adUiElements,
adEventListener, adEventListener,
imaFactory); imaFactory);
...@@ -298,6 +309,7 @@ public final class ImaAdsLoader ...@@ -298,6 +309,7 @@ public final class ImaAdsLoader
mediaLoadTimeoutMs, mediaLoadTimeoutMs,
mediaBitrate, mediaBitrate,
focusSkipButtonWhenAvailable, focusSkipButtonWhenAvailable,
playAdBeforeStartPosition,
adUiElements, adUiElements,
adEventListener, adEventListener,
imaFactory); imaFactory);
...@@ -331,6 +343,8 @@ public final class ImaAdsLoader ...@@ -331,6 +343,8 @@ public final class ImaAdsLoader
* milliseconds. * milliseconds.
*/ */
private static final long THRESHOLD_AD_PRELOAD_MS = 4000; private static final long THRESHOLD_AD_PRELOAD_MS = 4000;
/** The threshold below which ad cue points are treated as matching, in microseconds. */
private static final long THRESHOLD_AD_MATCH_US = 1000;
private static final int TIMEOUT_UNSET = -1; private static final int TIMEOUT_UNSET = -1;
private static final int BITRATE_UNSET = -1; private static final int BITRATE_UNSET = -1;
...@@ -345,12 +359,13 @@ public final class ImaAdsLoader ...@@ -345,12 +359,13 @@ public final class ImaAdsLoader
*/ */
private static final int IMA_AD_STATE_NONE = 0; private static final int IMA_AD_STATE_NONE = 0;
/** /**
* The ad playback state when IMA has called {@link #playAd(AdMediaInfo)} and not {@link * The ad playback state when IMA has called {@link ComponentListener#playAd(AdMediaInfo)} and not
* #pauseAd(AdMediaInfo)}. * {@link ComponentListener##pauseAd(AdMediaInfo)}.
*/ */
private static final int IMA_AD_STATE_PLAYING = 1; private static final int IMA_AD_STATE_PLAYING = 1;
/** /**
* The ad playback state when IMA has called {@link #pauseAd(AdMediaInfo)} while playing an ad. * The ad playback state when IMA has called {@link ComponentListener#pauseAd(AdMediaInfo)} while
* playing an ad.
*/ */
private static final int IMA_AD_STATE_PAUSED = 2; private static final int IMA_AD_STATE_PAUSED = 2;
...@@ -360,13 +375,15 @@ public final class ImaAdsLoader ...@@ -360,13 +375,15 @@ public final class ImaAdsLoader
private final int vastLoadTimeoutMs; private final int vastLoadTimeoutMs;
private final int mediaLoadTimeoutMs; private final int mediaLoadTimeoutMs;
private final boolean focusSkipButtonWhenAvailable; private final boolean focusSkipButtonWhenAvailable;
private final boolean playAdBeforeStartPosition;
private final int mediaBitrate; private final int mediaBitrate;
@Nullable private final Set<UiElement> adUiElements; @Nullable private final Set<UiElement> adUiElements;
@Nullable private final AdEventListener adEventListener; @Nullable private final AdEventListener adEventListener;
private final ImaFactory imaFactory; private final ImaFactory imaFactory;
private final Timeline.Period period; private final Timeline.Period period;
private final Handler handler; private final Handler handler;
private final List<VideoAdPlayerCallback> adCallbacks; private final ComponentListener componentListener;
private final List<VideoAdPlayer.VideoAdPlayerCallback> adCallbacks;
private final AdDisplayContainer adDisplayContainer; private final AdDisplayContainer adDisplayContainer;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
private final Runnable updateAdProgressRunnable; private final Runnable updateAdProgressRunnable;
...@@ -380,10 +397,10 @@ public final class ImaAdsLoader ...@@ -380,10 +397,10 @@ public final class ImaAdsLoader
@Nullable private Player player; @Nullable private Player player;
private VideoProgressUpdate lastContentProgress; private VideoProgressUpdate lastContentProgress;
private VideoProgressUpdate lastAdProgress; private VideoProgressUpdate lastAdProgress;
private int lastVolumePercentage; private int lastVolumePercent;
@Nullable private AdsManager adsManager; @Nullable private AdsManager adsManager;
private boolean initializedAdsManager; private boolean isAdsManagerInitialized;
private boolean hasAdPlaybackState; private boolean hasAdPlaybackState;
@Nullable private AdLoadException pendingAdLoadError; @Nullable private AdLoadException pendingAdLoadError;
private Timeline timeline; private Timeline timeline;
...@@ -423,10 +440,10 @@ public final class ImaAdsLoader ...@@ -423,10 +440,10 @@ public final class ImaAdsLoader
*/ */
@Nullable private AdInfo pendingAdPrepareErrorAdInfo; @Nullable private AdInfo pendingAdPrepareErrorAdInfo;
/** /**
* If a content period has finished but IMA has not yet called {@link #playAd(AdMediaInfo)}, * If a content period has finished but IMA has not yet called {@link
* stores the value of {@link SystemClock#elapsedRealtime()} when the content stopped playing. * ComponentListener#playAd(AdMediaInfo)}, stores the value of {@link
* This can be used to determine a fake, increasing content position. {@link C#TIME_UNSET} * SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to determine
* otherwise. * a fake, increasing content position. {@link C#TIME_UNSET} otherwise.
*/ */
private long fakeContentProgressElapsedRealtimeMs; private long fakeContentProgressElapsedRealtimeMs;
/** /**
...@@ -436,7 +453,10 @@ public final class ImaAdsLoader ...@@ -436,7 +453,10 @@ public final class ImaAdsLoader
private long fakeContentProgressOffsetMs; private long fakeContentProgressOffsetMs;
/** Stores the pending content position when a seek operation was intercepted to play an ad. */ /** Stores the pending content position when a seek operation was intercepted to play an ad. */
private long pendingContentPositionMs; private long pendingContentPositionMs;
/** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */ /**
* Whether {@link ComponentListener#getContentProgress()} has sent {@link
* #pendingContentPositionMs} to IMA.
*/
private boolean sentPendingContentPositionMs; private boolean sentPendingContentPositionMs;
/** /**
* Stores the real time in milliseconds at which the player started buffering, possibly due to not * Stores the real time in milliseconds at which the player started buffering, possibly due to not
...@@ -465,6 +485,7 @@ public final class ImaAdsLoader ...@@ -465,6 +485,7 @@ public final class ImaAdsLoader
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
/* mediaBitrate= */ BITRATE_UNSET, /* mediaBitrate= */ BITRATE_UNSET,
/* focusSkipButtonWhenAvailable= */ true, /* focusSkipButtonWhenAvailable= */ true,
/* playAdBeforeStartPosition= */ true,
/* adUiElements= */ null, /* adUiElements= */ null,
/* adEventListener= */ null, /* adEventListener= */ null,
/* imaFactory= */ new DefaultImaFactory()); /* imaFactory= */ new DefaultImaFactory());
...@@ -481,6 +502,7 @@ public final class ImaAdsLoader ...@@ -481,6 +502,7 @@ public final class ImaAdsLoader
int mediaLoadTimeoutMs, int mediaLoadTimeoutMs,
int mediaBitrate, int mediaBitrate,
boolean focusSkipButtonWhenAvailable, boolean focusSkipButtonWhenAvailable,
boolean playAdBeforeStartPosition,
@Nullable Set<UiElement> adUiElements, @Nullable Set<UiElement> adUiElements,
@Nullable AdEventListener adEventListener, @Nullable AdEventListener adEventListener,
ImaFactory imaFactory) { ImaFactory imaFactory) {
...@@ -492,6 +514,7 @@ public final class ImaAdsLoader ...@@ -492,6 +514,7 @@ public final class ImaAdsLoader
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
this.mediaBitrate = mediaBitrate; this.mediaBitrate = mediaBitrate;
this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable;
this.playAdBeforeStartPosition = playAdBeforeStartPosition;
this.adUiElements = adUiElements; this.adUiElements = adUiElements;
this.adEventListener = adEventListener; this.adEventListener = adEventListener;
this.imaFactory = imaFactory; this.imaFactory = imaFactory;
...@@ -505,14 +528,15 @@ public final class ImaAdsLoader ...@@ -505,14 +528,15 @@ public final class ImaAdsLoader
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
period = new Timeline.Period(); period = new Timeline.Period();
handler = Util.createHandler(getImaLooper(), /* callback= */ null); handler = Util.createHandler(getImaLooper(), /* callback= */ null);
componentListener = new ComponentListener();
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
adDisplayContainer = imaFactory.createAdDisplayContainer(); adDisplayContainer = imaFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); adDisplayContainer.setPlayer(/* videoAdPlayer= */ componentListener);
adsLoader = adsLoader =
imaFactory.createAdsLoader( imaFactory.createAdsLoader(
context.getApplicationContext(), imaSdkSettings, adDisplayContainer); context.getApplicationContext(), imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(/* adErrorListener= */ this); adsLoader.addAdErrorListener(componentListener);
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); adsLoader.addAdsLoadedListener(componentListener);
updateAdProgressRunnable = this::updateAdProgress; updateAdProgressRunnable = this::updateAdProgress;
adInfoByAdMediaInfo = new HashMap<>(); adInfoByAdMediaInfo = new HashMap<>();
supportedMimeTypes = Collections.emptyList(); supportedMimeTypes = Collections.emptyList();
...@@ -573,7 +597,7 @@ public final class ImaAdsLoader ...@@ -573,7 +597,7 @@ public final class ImaAdsLoader
if (vastLoadTimeoutMs != TIMEOUT_UNSET) { if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
request.setVastLoadTimeout(vastLoadTimeoutMs); request.setVastLoadTimeout(vastLoadTimeoutMs);
} }
request.setContentProgressProvider(this); request.setContentProgressProvider(componentListener);
pendingAdRequestContext = new Object(); pendingAdRequestContext = new Object();
request.setUserRequestContext(pendingAdRequestContext); request.setUserRequestContext(pendingAdRequestContext);
adsLoader.requestAds(request); adsLoader.requestAds(request);
...@@ -622,7 +646,7 @@ public final class ImaAdsLoader ...@@ -622,7 +646,7 @@ public final class ImaAdsLoader
player.addListener(this); player.addListener(this);
boolean playWhenReady = player.getPlayWhenReady(); boolean playWhenReady = player.getPlayWhenReady();
this.eventListener = eventListener; this.eventListener = eventListener;
lastVolumePercentage = 0; lastVolumePercent = 0;
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
ViewGroup adViewGroup = adViewProvider.getAdViewGroup(); ViewGroup adViewGroup = adViewProvider.getAdViewGroup();
...@@ -639,7 +663,7 @@ public final class ImaAdsLoader ...@@ -639,7 +663,7 @@ public final class ImaAdsLoader
adsManager.resume(); adsManager.resume();
} }
} else if (adsManager != null) { } else if (adsManager != null) {
adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints());
updateAdPlaybackState(); updateAdPlaybackState();
} else { } else {
// Ads haven't loaded yet, so request them. // Ads haven't loaded yet, so request them.
...@@ -659,9 +683,9 @@ public final class ImaAdsLoader ...@@ -659,9 +683,9 @@ public final class ImaAdsLoader
adPlaybackState.withAdResumePositionUs( adPlaybackState.withAdResumePositionUs(
playingAd ? C.msToUs(player.getCurrentPosition()) : 0); playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
} }
lastVolumePercentage = getVolume(); lastVolumePercent = getPlayerVolumePercent();
lastAdProgress = getAdVideoProgressUpdate(); lastAdProgress = getAdVideoProgressUpdate();
lastContentProgress = getContentProgress(); lastContentProgress = getContentVideoProgressUpdate();
adDisplayContainer.unregisterAllVideoControlsOverlays(); adDisplayContainer.unregisterAllVideoControlsOverlays();
player.removeListener(this); player.removeListener(this);
this.player = null; this.player = null;
...@@ -671,17 +695,9 @@ public final class ImaAdsLoader ...@@ -671,17 +695,9 @@ public final class ImaAdsLoader
@Override @Override
public void release() { public void release() {
pendingAdRequestContext = null; pendingAdRequestContext = null;
if (adsManager != null) { destroyAdsManager();
adsManager.removeAdErrorListener(this); adsLoader.removeAdsLoadedListener(componentListener);
adsManager.removeAdEventListener(this); adsLoader.removeAdErrorListener(componentListener);
if (adEventListener != null) {
adsManager.removeAdEventListener(adEventListener);
}
adsManager.destroy();
adsManager = null;
}
adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this);
adsLoader.removeAdErrorListener(/* adErrorListener= */ this);
imaPausedContent = false; imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE; imaAdState = IMA_AD_STATE_NONE;
imaAdMediaInfo = null; imaAdMediaInfo = null;
...@@ -689,7 +705,7 @@ public final class ImaAdsLoader ...@@ -689,7 +705,7 @@ public final class ImaAdsLoader
imaAdInfo = null; imaAdInfo = null;
pendingAdLoadError = null; pendingAdLoadError = null;
adPlaybackState = AdPlaybackState.NONE; adPlaybackState = AdPlaybackState.NONE;
hasAdPlaybackState = false; hasAdPlaybackState = true;
updateAdPlaybackState(); updateAdPlaybackState();
} }
...@@ -700,271 +716,11 @@ public final class ImaAdsLoader ...@@ -700,271 +716,11 @@ public final class ImaAdsLoader
} }
try { try {
handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception); handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception);
} catch (Exception e) { } catch (RuntimeException e) {
maybeNotifyInternalError("handlePrepareError", e); maybeNotifyInternalError("handlePrepareError", e);
} }
} }
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.
@Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
AdsManager adsManager = adsManagerLoadedEvent.getAdsManager();
if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) {
adsManager.destroy();
return;
}
pendingAdRequestContext = null;
this.adsManager = adsManager;
adsManager.addAdErrorListener(this);
adsManager.addAdEventListener(this);
if (adEventListener != null) {
adsManager.addAdEventListener(adEventListener);
}
if (player != null) {
// If a player is attached already, start playback immediately.
try {
adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints()));
hasAdPlaybackState = true;
updateAdPlaybackState();
} catch (Exception e) {
maybeNotifyInternalError("onAdsManagerLoaded", e);
}
}
}
// AdEvent.AdEventListener implementation.
@Override
public void onAdEvent(AdEvent adEvent) {
AdEventType adEventType = adEvent.getType();
if (DEBUG && adEventType != AdEventType.AD_PROGRESS) {
Log.d(TAG, "onAdEvent: " + adEventType);
}
if (adsManager == null) {
// Drop events after release.
return;
}
try {
handleAdEvent(adEvent);
} catch (Exception e) {
maybeNotifyInternalError("onAdEvent", e);
}
}
// AdErrorEvent.AdErrorListener implementation.
@Override
public void onAdError(AdErrorEvent adErrorEvent) {
AdError error = adErrorEvent.getError();
if (DEBUG) {
Log.d(TAG, "onAdError", error);
}
if (adsManager == null) {
// No ads were loaded, so allow playback to start without any ads.
pendingAdRequestContext = null;
adPlaybackState = AdPlaybackState.NONE;
hasAdPlaybackState = true;
updateAdPlaybackState();
} else if (isAdGroupLoadError(error)) {
try {
handleAdGroupLoadError(error);
} catch (Exception e) {
maybeNotifyInternalError("onAdError", e);
}
}
if (pendingAdLoadError == null) {
pendingAdLoadError = AdLoadException.createForAllAds(error);
}
maybeNotifyPendingAdLoadError();
}
// ContentProgressProvider implementation.
@Override
public VideoProgressUpdate getContentProgress() {
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
if (DEBUG) {
Log.d(TAG, "Content progress: " + videoProgressUpdate);
}
if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) {
// IMA is polling the player position but we are buffering for an ad to preload, so playback
// may be stuck. Detect this case and signal an error if applicable.
long stuckElapsedRealtimeMs =
SystemClock.elapsedRealtime() - waitingForPreloadElapsedRealtimeMs;
if (stuckElapsedRealtimeMs >= THRESHOLD_AD_PRELOAD_MS) {
waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET;
handleAdGroupLoadError(new IOException("Ad preloading timed out"));
maybeNotifyPendingAdLoadError();
}
}
return videoProgressUpdate;
}
// VideoAdPlayer implementation.
@Override
public VideoProgressUpdate getAdProgress() {
throw new IllegalStateException("Unexpected call to getAdProgress when using preloading");
}
@Override
public int getVolume() {
@Nullable Player player = this.player;
if (player == null) {
return lastVolumePercentage;
}
@Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
if (audioComponent != null) {
return (int) (audioComponent.getVolume() * 100);
}
// Check for a selected track using an audio renderer.
TrackSelectionArray trackSelections = player.getCurrentTrackSelections();
for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) {
if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) {
return 100;
}
}
return 0;
}
@Override
public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) {
try {
if (DEBUG) {
Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo);
}
if (adsManager == null) {
// Drop events after release.
return;
}
int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo);
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
adInfoByAdMediaInfo.put(adMediaInfo, adInfo);
if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) {
// We have already marked this ad as having failed to load, so ignore the request. IMA will
// timeout after its media load timeout.
return;
}
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex];
if (adGroup.count == C.LENGTH_UNSET) {
adPlaybackState =
adPlaybackState.withAdCount(
adInfo.adGroupIndex, Math.max(adPodInfo.getTotalAds(), adGroup.states.length));
adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex];
}
for (int i = 0; i < adIndexInAdGroup; i++) {
// Any preceding ads that haven't loaded are not going to load.
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
adPlaybackState =
adPlaybackState.withAdLoadError(
/* adGroupIndex= */ adGroupIndex, /* adIndexInAdGroup= */ i);
}
}
Uri adUri = Uri.parse(adMediaInfo.getUrl());
adPlaybackState =
adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri);
updateAdPlaybackState();
} catch (Exception e) {
maybeNotifyInternalError("loadAd", e);
}
}
@Override
public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
adCallbacks.add(videoAdPlayerCallback);
}
@Override
public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
adCallbacks.remove(videoAdPlayerCallback);
}
@Override
public void playAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
// Drop events after release.
return;
}
if (imaAdState == IMA_AD_STATE_PLAYING) {
// IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
Log.w(TAG, "Unexpected playAd without stopAd");
}
if (imaAdState == IMA_AD_STATE_NONE) {
// IMA is requesting to play the ad, so stop faking the content position.
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
fakeContentProgressOffsetMs = C.TIME_UNSET;
imaAdState = IMA_AD_STATE_PLAYING;
imaAdMediaInfo = adMediaInfo;
imaAdInfo = Assertions.checkNotNull(adInfoByAdMediaInfo.get(adMediaInfo));
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPlay(adMediaInfo);
}
if (pendingAdPrepareErrorAdInfo != null && pendingAdPrepareErrorAdInfo.equals(imaAdInfo)) {
pendingAdPrepareErrorAdInfo = null;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError(adMediaInfo);
}
}
updateAdProgress();
} else {
imaAdState = IMA_AD_STATE_PLAYING;
Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo));
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onResume(adMediaInfo);
}
}
if (!Assertions.checkNotNull(player).getPlayWhenReady()) {
Assertions.checkNotNull(adsManager).pause();
}
}
@Override
public void stopAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
// Drop event after release.
return;
}
Assertions.checkNotNull(player);
Assertions.checkState(imaAdState != IMA_AD_STATE_NONE);
try {
stopAdInternal();
} catch (Exception e) {
maybeNotifyInternalError("stopAd", e);
}
}
@Override
public void pauseAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo));
}
if (imaAdState == IMA_AD_STATE_NONE) {
// This method is called after content is resumed.
return;
}
Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo));
imaAdState = IMA_AD_STATE_PAUSED;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPause(adMediaInfo);
}
}
// Player.EventListener implementation. // Player.EventListener implementation.
@Override @Override
...@@ -980,9 +736,21 @@ public final class ImaAdsLoader ...@@ -980,9 +736,21 @@ public final class ImaAdsLoader
if (contentDurationUs != C.TIME_UNSET) { if (contentDurationUs != C.TIME_UNSET) {
adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);
} }
if (!initializedAdsManager && adsManager != null) { @Nullable AdsManager adsManager = this.adsManager;
initializedAdsManager = true; if (!isAdsManagerInitialized && adsManager != null) {
initializeAdsManager(adsManager); isAdsManagerInitialized = true;
@Nullable AdsRenderingSettings adsRenderingSettings = setupAdsRendering();
if (adsRenderingSettings == null) {
// There are no ads to play.
destroyAdsManager();
} else {
adsManager.init(adsRenderingSettings);
adsManager.start();
if (DEBUG) {
Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings);
}
}
updateAdPlaybackState();
} }
handleTimelineOrPositionChanged(); handleTimelineOrPositionChanged();
} }
...@@ -1047,7 +815,12 @@ public final class ImaAdsLoader ...@@ -1047,7 +815,12 @@ public final class ImaAdsLoader
// Internal methods. // Internal methods.
private void initializeAdsManager(AdsManager adsManager) { /**
* Configures ads rendering for starting playback, returning the settings for the IMA SDK or
* {@code null} if no ads should play.
*/
@Nullable
private AdsRenderingSettings setupAdsRendering() {
AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
adsRenderingSettings.setEnablePreloading(true); adsRenderingSettings.setEnablePreloading(true);
adsRenderingSettings.setMimeTypes(supportedMimeTypes); adsRenderingSettings.setMimeTypes(supportedMimeTypes);
...@@ -1063,36 +836,48 @@ public final class ImaAdsLoader ...@@ -1063,36 +836,48 @@ public final class ImaAdsLoader
} }
// Skip ads based on the start position as required. // Skip ads based on the start position as required.
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); long[] adGroupTimesUs = adPlaybackState.adGroupTimesUs;
long contentPositionMs = long contentPositionMs =
getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period); getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period);
int adGroupIndexForPosition = int adGroupForPositionIndex =
adPlaybackState.getAdGroupIndexForPositionUs( adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) { if (adGroupForPositionIndex != C.INDEX_UNSET) {
// Skip any ad groups before the one at or immediately before the playback position. boolean playAdWhenStartingPlayback =
for (int i = 0; i < adGroupIndexForPosition; i++) { playAdBeforeStartPosition
adPlaybackState = adPlaybackState.withSkippedAdGroup(i); || adGroupTimesUs[adGroupForPositionIndex] == C.msToUs(contentPositionMs);
if (!playAdWhenStartingPlayback) {
adGroupForPositionIndex++;
} else if (hasMidrollAdGroups(adGroupTimesUs)) {
// Provide the player's initial position to trigger loading and playing the ad. If there are
// no midrolls, we are playing a preroll and any pending content position wouldn't be
// cleared.
pendingContentPositionMs = contentPositionMs;
}
if (adGroupForPositionIndex > 0) {
for (int i = 0; i < adGroupForPositionIndex; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
if (adGroupForPositionIndex == adGroupTimesUs.length) {
// We don't need to play any ads. Because setPlayAdsAfterTime does not discard non-VMAP
// ads, we signal that no ads will render so the caller can destroy the ads manager.
return null;
}
long adGroupForPositionTimeUs = adGroupTimesUs[adGroupForPositionIndex];
long adGroupBeforePositionTimeUs = adGroupTimesUs[adGroupForPositionIndex - 1];
if (adGroupForPositionTimeUs == C.TIME_END_OF_SOURCE) {
// Play the postroll by offsetting the start position just past the last non-postroll ad.
adsRenderingSettings.setPlayAdsAfterTime(
(double) adGroupBeforePositionTimeUs / C.MICROS_PER_SECOND + 1d);
} else {
// Play ads after the midpoint between the ad to play and the one before it, to avoid
// issues with rounding one of the two ad times.
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforePositionTimeUs) / 2d;
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
}
} }
// Play ads after the midpoint between the ad to play and the one before it, to avoid issues
// with rounding one of the two ad times.
long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition];
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
}
if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) {
// Provide the player's initial position to trigger loading and playing the ad.
pendingContentPositionMs = contentPositionMs;
}
adsManager.init(adsRenderingSettings);
adsManager.start();
updateAdPlaybackState();
if (DEBUG) {
Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings);
} }
return adsRenderingSettings;
} }
private void handleAdEvent(AdEvent adEvent) { private void handleAdEvent(AdEvent adEvent) {
...@@ -1203,6 +988,27 @@ public final class ImaAdsLoader ...@@ -1203,6 +988,27 @@ public final class ImaAdsLoader
handler.removeCallbacks(updateAdProgressRunnable); handler.removeCallbacks(updateAdProgressRunnable);
} }
private int getPlayerVolumePercent() {
@Nullable Player player = this.player;
if (player == null) {
return lastVolumePercent;
}
@Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
if (audioComponent != null) {
return (int) (audioComponent.getVolume() * 100);
}
// Check for a selected track using an audio renderer.
TrackSelectionArray trackSelections = player.getCurrentTrackSelections();
for (int i = 0; i < player.getRendererCount() && i < trackSelections.length; i++) {
if (player.getRendererType(i) == C.TRACK_TYPE_AUDIO && trackSelections.get(i) != null) {
return 100;
}
}
return 0;
}
private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) { if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) {
if (!bufferingAd && playbackState == Player.STATE_BUFFERING) { if (!bufferingAd && playbackState == Player.STATE_BUFFERING) {
...@@ -1283,11 +1089,19 @@ public final class ImaAdsLoader ...@@ -1283,11 +1089,19 @@ public final class ImaAdsLoader
} }
if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) { if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) {
int adGroupIndex = player.getCurrentAdGroupIndex(); int adGroupIndex = player.getCurrentAdGroupIndex();
// IMA hasn't called playAd yet, so fake the content position. if (adPlaybackState.adGroupTimesUs[adGroupIndex] == C.TIME_END_OF_SOURCE) {
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); adsLoader.contentComplete();
fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); if (DEBUG) {
if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { Log.d(TAG, "adsLoader.contentComplete from period transition");
fakeContentProgressOffsetMs = contentDurationMs; }
sentContentComplete = true;
} else {
// IMA hasn't called playAd yet, so fake the content position.
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
fakeContentProgressOffsetMs = contentDurationMs;
}
} }
} }
} }
...@@ -1406,7 +1220,7 @@ public final class ImaAdsLoader ...@@ -1406,7 +1220,7 @@ public final class ImaAdsLoader
&& positionMs + THRESHOLD_END_OF_CONTENT_MS >= contentDurationMs) { && positionMs + THRESHOLD_END_OF_CONTENT_MS >= contentDurationMs) {
adsLoader.contentComplete(); adsLoader.contentComplete();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "adsLoader.contentComplete"); Log.d(TAG, "adsLoader.contentComplete from content position check");
} }
sentContentComplete = true; sentContentComplete = true;
} }
...@@ -1448,9 +1262,15 @@ public final class ImaAdsLoader ...@@ -1448,9 +1262,15 @@ public final class ImaAdsLoader
} }
// adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead. // adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead.
long adGroupTimeUs = (long) (((float) adPodInfo.getTimeOffset()) * C.MICROS_PER_SECOND); // We receive cue points from IMA SDK as floats. This code replicates the same calculation used
// to populate adGroupTimesUs (having truncated input back to float, to avoid failures if the
// behavior of the IMA SDK changes to provide greater precision in AdPodInfo).
long adPodTimeUs =
Math.round((double) ((float) adPodInfo.getTimeOffset()) * C.MICROS_PER_SECOND);
for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) { for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) {
if (adPlaybackState.adGroupTimesUs[adGroupIndex] == adGroupTimeUs) { long adGroupTimeUs = adPlaybackState.adGroupTimesUs[adGroupIndex];
if (adGroupTimeUs != C.TIME_END_OF_SOURCE
&& Math.abs(adGroupTimeUs - adPodTimeUs) < THRESHOLD_AD_MATCH_US) {
return adGroupIndex; return adGroupIndex;
} }
} }
...@@ -1492,28 +1312,6 @@ public final class ImaAdsLoader ...@@ -1492,28 +1312,6 @@ public final class ImaAdsLoader
: timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs()); : timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs());
} }
private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad.
return new long[] {0};
}
int count = cuePoints.size();
long[] adGroupTimesUs = new long[count];
int adGroupIndex = 0;
for (int i = 0; i < count; i++) {
double cuePoint = cuePoints.get(i);
if (cuePoint == -1.0) {
adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE;
} else {
adGroupTimesUs[adGroupIndex++] = (long) (C.MICROS_PER_SECOND * cuePoint);
}
}
// Cue points may be out of order, so sort them.
Arrays.sort(adGroupTimesUs, 0, adGroupIndex);
return adGroupTimesUs;
}
private static boolean isAdGroupLoadError(AdError adError) { private static boolean isAdGroupLoadError(AdError adError) {
// TODO: Find out what other errors need to be handled (if any), and whether each one relates to // TODO: Find out what other errors need to be handled (if any), and whether each one relates to
// a single ad, ad group or the whole timeline. // a single ad, ad group or the whole timeline.
...@@ -1539,6 +1337,18 @@ public final class ImaAdsLoader ...@@ -1539,6 +1337,18 @@ public final class ImaAdsLoader
} }
} }
private void destroyAdsManager() {
if (adsManager != null) {
adsManager.removeAdErrorListener(componentListener);
adsManager.removeAdEventListener(componentListener);
if (adEventListener != null) {
adsManager.removeAdEventListener(adEventListener);
}
adsManager.destroy();
adsManager = null;
}
}
/** Factory for objects provided by the IMA SDK. */ /** Factory for objects provided by the IMA SDK. */
@VisibleForTesting @VisibleForTesting
/* package */ interface ImaFactory { /* package */ interface ImaFactory {
...@@ -1555,6 +1365,299 @@ public final class ImaAdsLoader ...@@ -1555,6 +1365,299 @@ public final class ImaAdsLoader
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
} }
private final class ComponentListener
implements VideoAdPlayer,
ContentProgressProvider,
AdErrorListener,
AdsLoadedListener,
AdEventListener {
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.
@Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
AdsManager adsManager = adsManagerLoadedEvent.getAdsManager();
if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) {
adsManager.destroy();
return;
}
pendingAdRequestContext = null;
ImaAdsLoader.this.adsManager = adsManager;
adsManager.addAdErrorListener(this);
adsManager.addAdEventListener(this);
if (adEventListener != null) {
adsManager.addAdEventListener(adEventListener);
}
if (player != null) {
// If a player is attached already, start playback immediately.
try {
adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints());
hasAdPlaybackState = true;
updateAdPlaybackState();
} catch (RuntimeException e) {
maybeNotifyInternalError("onAdsManagerLoaded", e);
}
}
}
// AdEvent.AdEventListener implementation.
@Override
public void onAdEvent(AdEvent adEvent) {
AdEventType adEventType = adEvent.getType();
if (DEBUG && adEventType != AdEventType.AD_PROGRESS) {
Log.d(TAG, "onAdEvent: " + adEventType);
}
if (adsManager == null) {
// Drop events after release.
return;
}
try {
handleAdEvent(adEvent);
} catch (RuntimeException e) {
maybeNotifyInternalError("onAdEvent", e);
}
}
// AdErrorEvent.AdErrorListener implementation.
@Override
public void onAdError(AdErrorEvent adErrorEvent) {
AdError error = adErrorEvent.getError();
if (DEBUG) {
Log.d(TAG, "onAdError", error);
}
if (adsManager == null) {
// No ads were loaded, so allow playback to start without any ads.
pendingAdRequestContext = null;
adPlaybackState = AdPlaybackState.NONE;
hasAdPlaybackState = true;
updateAdPlaybackState();
} else if (isAdGroupLoadError(error)) {
try {
handleAdGroupLoadError(error);
} catch (RuntimeException e) {
maybeNotifyInternalError("onAdError", e);
}
}
if (pendingAdLoadError == null) {
pendingAdLoadError = AdLoadException.createForAllAds(error);
}
maybeNotifyPendingAdLoadError();
}
// ContentProgressProvider implementation.
@Override
public VideoProgressUpdate getContentProgress() {
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
if (DEBUG) {
Log.d(TAG, "Content progress: " + videoProgressUpdate);
}
if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) {
// IMA is polling the player position but we are buffering for an ad to preload, so playback
// may be stuck. Detect this case and signal an error if applicable.
long stuckElapsedRealtimeMs =
SystemClock.elapsedRealtime() - waitingForPreloadElapsedRealtimeMs;
if (stuckElapsedRealtimeMs >= THRESHOLD_AD_PRELOAD_MS) {
waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET;
handleAdGroupLoadError(new IOException("Ad preloading timed out"));
maybeNotifyPendingAdLoadError();
}
}
return videoProgressUpdate;
}
// VideoAdPlayer implementation.
@Override
public VideoProgressUpdate getAdProgress() {
throw new IllegalStateException("Unexpected call to getAdProgress when using preloading");
}
@Override
public int getVolume() {
return getPlayerVolumePercent();
}
@Override
public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) {
try {
if (adsManager == null) {
// Drop events after release.
if (DEBUG) {
Log.d(
TAG,
"loadAd after release "
+ getAdMediaInfoString(adMediaInfo)
+ ", ad pod "
+ adPodInfo);
}
return;
}
int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo);
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
adInfoByAdMediaInfo.put(adMediaInfo, adInfo);
if (DEBUG) {
Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo));
}
if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) {
// We have already marked this ad as having failed to load, so ignore the request. IMA
// will timeout after its media load timeout.
return;
}
// The ad count may increase on successive loads of ads in the same ad pod, for example, due
// to separate requests for ad tags with multiple ads within the ad pod completing after an
// earlier ad has loaded. See also https://github.com/google/ExoPlayer/issues/7477.
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex];
adPlaybackState =
adPlaybackState.withAdCount(
adInfo.adGroupIndex, Math.max(adPodInfo.getTotalAds(), adGroup.states.length));
adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex];
for (int i = 0; i < adIndexInAdGroup; i++) {
// Any preceding ads that haven't loaded are not going to load.
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
adPlaybackState =
adPlaybackState.withAdLoadError(adGroupIndex, /* adIndexInAdGroup= */ i);
}
}
Uri adUri = Uri.parse(adMediaInfo.getUrl());
adPlaybackState =
adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri);
updateAdPlaybackState();
} catch (RuntimeException e) {
maybeNotifyInternalError("loadAd", e);
}
}
@Override
public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
adCallbacks.add(videoAdPlayerCallback);
}
@Override
public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
adCallbacks.remove(videoAdPlayerCallback);
}
@Override
public void playAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
// Drop events after release.
return;
}
if (imaAdState == IMA_AD_STATE_PLAYING) {
// IMA does not always call stopAd before resuming content.
// See [Internal: b/38354028].
Log.w(TAG, "Unexpected playAd without stopAd");
}
try {
if (imaAdState == IMA_AD_STATE_NONE) {
// IMA is requesting to play the ad, so stop faking the content position.
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
fakeContentProgressOffsetMs = C.TIME_UNSET;
imaAdState = IMA_AD_STATE_PLAYING;
imaAdMediaInfo = adMediaInfo;
imaAdInfo = Assertions.checkNotNull(adInfoByAdMediaInfo.get(adMediaInfo));
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPlay(adMediaInfo);
}
if (pendingAdPrepareErrorAdInfo != null
&& pendingAdPrepareErrorAdInfo.equals(imaAdInfo)) {
pendingAdPrepareErrorAdInfo = null;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError(adMediaInfo);
}
}
updateAdProgress();
} else {
imaAdState = IMA_AD_STATE_PLAYING;
Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo));
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onResume(adMediaInfo);
}
}
if (!Assertions.checkNotNull(player).getPlayWhenReady()) {
Assertions.checkNotNull(adsManager).pause();
}
} catch (RuntimeException e) {
maybeNotifyInternalError("playAd", e);
}
}
@Override
public void stopAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
// Drop event after release.
return;
}
if (imaAdState == IMA_AD_STATE_NONE) {
// This method is called if loadAd has been called but the preloaded ad won't play due to a
// seek to a different position, so drop the event and discard the ad. See also [Internal:
// b/159111848].
@Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo);
if (adInfo != null) {
adPlaybackState =
adPlaybackState.withSkippedAd(adInfo.adGroupIndex, adInfo.adIndexInAdGroup);
updateAdPlaybackState();
}
return;
}
try {
Assertions.checkNotNull(player);
stopAdInternal();
} catch (RuntimeException e) {
maybeNotifyInternalError("stopAd", e);
}
}
@Override
public void pauseAd(AdMediaInfo adMediaInfo) {
if (DEBUG) {
Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
// Drop event after release.
return;
}
if (imaAdState == IMA_AD_STATE_NONE) {
// This method is called if loadAd has been called but the loaded ad won't play due to a
// seek to a different position, so drop the event. See also [Internal: b/159111848].
return;
}
try {
Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo));
imaAdState = IMA_AD_STATE_PAUSED;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPause(adMediaInfo);
}
} catch (RuntimeException e) {
maybeNotifyInternalError("pauseAd", e);
}
}
@Override
public void release() {
// Do nothing.
}
}
// TODO: Consider moving this into AdPlaybackState. // TODO: Consider moving this into AdPlaybackState.
private static final class AdInfo { private static final class AdInfo {
public final int adGroupIndex; public final int adGroupIndex;
......
...@@ -17,10 +17,12 @@ package com.google.android.exoplayer2.ext.ima; ...@@ -17,10 +17,12 @@ package com.google.android.exoplayer2.ext.ima;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
...@@ -42,6 +44,8 @@ import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; ...@@ -42,6 +44,8 @@ import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest; import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo; import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
...@@ -56,6 +60,7 @@ import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; ...@@ -56,6 +60,7 @@ import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.io.IOException; import java.io.IOException;
import java.time.Duration; import java.time.Duration;
...@@ -91,8 +96,7 @@ public final class ImaAdsLoaderTest { ...@@ -91,8 +96,7 @@ public final class ImaAdsLoaderTest {
private static final Uri TEST_URI = Uri.EMPTY; private static final Uri TEST_URI = Uri.EMPTY;
private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString()); private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString());
private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND; private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;
private static final long[][] ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}}; private static final ImmutableList<Float> PREROLL_CUE_POINTS_SECONDS = ImmutableList.of(0f);
private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f};
@Rule public final MockitoRule mockito = MockitoJUnit.rule(); @Rule public final MockitoRule mockito = MockitoJUnit.rule();
...@@ -111,6 +115,9 @@ public final class ImaAdsLoaderTest { ...@@ -111,6 +115,9 @@ public final class ImaAdsLoaderTest {
private ViewGroup adViewGroup; private ViewGroup adViewGroup;
private View adOverlayView; private View adOverlayView;
private AdsLoader.AdViewProvider adViewProvider; private AdsLoader.AdViewProvider adViewProvider;
private AdEvent.AdEventListener adEventListener;
private ContentProgressProvider contentProgressProvider;
private VideoAdPlayer videoAdPlayer;
private TestAdsLoaderListener adsLoaderListener; private TestAdsLoaderListener adsLoaderListener;
private FakePlayer fakeExoPlayer; private FakePlayer fakeExoPlayer;
private ImaAdsLoader imaAdsLoader; private ImaAdsLoader imaAdsLoader;
...@@ -144,14 +151,14 @@ public final class ImaAdsLoaderTest { ...@@ -144,14 +151,14 @@ public final class ImaAdsLoaderTest {
@Test @Test
public void builder_overridesPlayerType() { public void builder_overridesPlayerType() {
when(mockImaSdkSettings.getPlayerType()).thenReturn("test player type"); when(mockImaSdkSettings.getPlayerType()).thenReturn("test player type");
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
verify(mockImaSdkSettings).setPlayerType("google/exo.ext.ima"); verify(mockImaSdkSettings).setPlayerType("google/exo.ext.ima");
} }
@Test @Test
public void start_setsAdUiViewGroup() { public void start_setsAdUiViewGroup() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup); verify(mockAdDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup);
...@@ -161,7 +168,7 @@ public final class ImaAdsLoaderTest { ...@@ -161,7 +168,7 @@ public final class ImaAdsLoaderTest {
@Test @Test
public void start_withPlaceholderContent_initializedAdsLoader() { public void start_withPlaceholderContent_initializedAdsLoader() {
Timeline placeholderTimeline = new DummyTimeline(/* tag= */ null); Timeline placeholderTimeline = new DummyTimeline(/* tag= */ null);
setupPlayback(placeholderTimeline, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); setupPlayback(placeholderTimeline, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
// We'll only create the rendering settings when initializing the ads loader. // We'll only create the rendering settings when initializing the ads loader.
...@@ -170,26 +177,27 @@ public final class ImaAdsLoaderTest { ...@@ -170,26 +177,27 @@ public final class ImaAdsLoaderTest {
@Test @Test
public void start_updatesAdPlaybackState() { public void start_updatesAdPlaybackState() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
assertThat(adsLoaderListener.adPlaybackState) assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo( .isEqualTo(
new AdPlaybackState(/* adGroupTimesUs...= */ 0) new AdPlaybackState(/* adGroupTimesUs...= */ 0)
.withAdDurationsUs(ADS_DURATIONS_US)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)); .withContentDurationUs(CONTENT_PERIOD_DURATION_US));
} }
@Test @Test
public void startAfterRelease() { public void startAfterRelease() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release(); imaAdsLoader.release();
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
} }
@Test @Test
public void startAndCallbacksAfterRelease() { public void startAndCallbacksAfterRelease() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
// Request ads in order to get a reference to the ad event listener.
imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.release(); imaAdsLoader.release();
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0); fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
...@@ -200,47 +208,47 @@ public final class ImaAdsLoaderTest { ...@@ -200,47 +208,47 @@ public final class ImaAdsLoaderTest {
// when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA // when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA
// SDK being proguarded. // SDK being proguarded.
imaAdsLoader.requestAds(adViewGroup); imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo); videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
imaAdsLoader.playAd(TEST_AD_MEDIA_INFO); videoAdPlayer.playAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
imaAdsLoader.pauseAd(TEST_AD_MEDIA_INFO); videoAdPlayer.pauseAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO); videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException())); imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException()));
imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
imaAdsLoader.handlePrepareError( imaAdsLoader.handlePrepareError(
/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException()); /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException());
} }
@Test @Test
public void playback_withPrerollAd_marksAdAsPlayed() { public void playback_withPrerollAd_marksAdAsPlayed() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
// Load the preroll ad. // Load the preroll ad.
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo); videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
// Play the preroll ad. // Play the preroll ad.
imaAdsLoader.playAd(TEST_AD_MEDIA_INFO); videoAdPlayer.playAd(TEST_AD_MEDIA_INFO);
fakeExoPlayer.setPlayingAdPosition( fakeExoPlayer.setPlayingAdPosition(
/* adGroupIndex= */ 0, /* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0, /* adIndexInAdGroup= */ 0,
/* position= */ 0, /* position= */ 0,
/* contentPosition= */ 0); /* contentPosition= */ 0);
fakeExoPlayer.setState(Player.STATE_READY, true); fakeExoPlayer.setState(Player.STATE_READY, true);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd));
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd));
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd)); adEventListener.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd));
// Play the content. // Play the content.
fakeExoPlayer.setPlayingContentPosition(0); fakeExoPlayer.setPlayingContentPosition(0);
imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO); videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
// Verify that the preroll ad has been marked as played. // Verify that the preroll ad has been marked as played.
assertThat(adsLoaderListener.adPlaybackState) assertThat(adsLoaderListener.adPlaybackState)
...@@ -249,24 +257,24 @@ public final class ImaAdsLoaderTest { ...@@ -249,24 +257,24 @@ public final class ImaAdsLoaderTest {
.withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI) .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI)
.withAdDurationsUs(ADS_DURATIONS_US) .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withAdResumePositionUs(/* adResumePositionUs= */ 0)); .withAdResumePositionUs(/* adResumePositionUs= */ 0));
} }
@Test @Test
public void playback_withPostrollFetchError_marksAdAsInErrorState() { public void playback_withPostrollFetchError_marksAdAsInErrorState() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, new Float[] {-1f}); setupPlayback(CONTENT_TIMELINE, ImmutableList.of(-1f));
// Simulate loading an empty postroll ad. // Simulate loading an empty postroll ad.
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.onAdEvent(mockPostrollFetchErrorAdEvent); adEventListener.onAdEvent(mockPostrollFetchErrorAdEvent);
assertThat(adsLoaderListener.adPlaybackState) assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo( .isEqualTo(
new AdPlaybackState(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE) new AdPlaybackState(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdDurationsUs(ADS_DURATIONS_US) .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)); .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
} }
...@@ -275,10 +283,9 @@ public final class ImaAdsLoaderTest { ...@@ -275,10 +283,9 @@ public final class ImaAdsLoaderTest {
public void playback_withAdNotPreloadingBeforeTimeout_hasNoError() { public void playback_withAdNotPreloadingBeforeTimeout_hasNoError() {
// Simulate an ad at 2 seconds. // Simulate an ad at 2 seconds.
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND; long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
setupPlayback( long adGroupTimeUs = adGroupPositionInWindowUs;
CONTENT_TIMELINE, ImmutableList<Float> cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND);
ADS_DURATIONS_US, setupPlayback(CONTENT_TIMELINE, cuePoints);
new Float[] {(float) adGroupPositionInWindowUs / C.MICROS_PER_SECOND});
// Advance playback to just before the midroll and simulate buffering. // Advance playback to just before the midroll and simulate buffering.
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
...@@ -286,23 +293,21 @@ public final class ImaAdsLoaderTest { ...@@ -286,23 +293,21 @@ public final class ImaAdsLoaderTest {
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
// Advance before the timeout and simulating polling content progress. // Advance before the timeout and simulating polling content progress.
ShadowSystemClock.advanceBy(Duration.ofSeconds(1)); ShadowSystemClock.advanceBy(Duration.ofSeconds(1));
imaAdsLoader.getContentProgress(); contentProgressProvider.getContentProgress();
assertThat(adsLoaderListener.adPlaybackState) assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo( .isEqualTo(
new AdPlaybackState(/* adGroupTimesUs...= */ adGroupPositionInWindowUs) AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withContentDurationUs(CONTENT_PERIOD_DURATION_US));
.withAdDurationsUs(ADS_DURATIONS_US));
} }
@Test @Test
public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() { public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() {
// Simulate an ad at 2 seconds. // Simulate an ad at 2 seconds.
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND; long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
setupPlayback( long adGroupTimeUs = adGroupPositionInWindowUs;
CONTENT_TIMELINE, ImmutableList<Float> cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND);
ADS_DURATIONS_US, setupPlayback(CONTENT_TIMELINE, cuePoints);
new Float[] {(float) adGroupPositionInWindowUs / C.MICROS_PER_SECOND});
// Advance playback to just before the midroll and simulate buffering. // Advance playback to just before the midroll and simulate buffering.
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
...@@ -310,20 +315,296 @@ public final class ImaAdsLoaderTest { ...@@ -310,20 +315,296 @@ public final class ImaAdsLoaderTest {
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
// Advance past the timeout and simulate polling content progress. // Advance past the timeout and simulate polling content progress.
ShadowSystemClock.advanceBy(Duration.ofSeconds(5)); ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
imaAdsLoader.getContentProgress(); contentProgressProvider.getContentProgress();
assertThat(adsLoaderListener.adPlaybackState) assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo( .isEqualTo(
new AdPlaybackState(/* adGroupTimesUs...= */ adGroupPositionInWindowUs) AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdDurationsUs(ADS_DURATIONS_US) .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)); .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
} }
@Test @Test
public void resumePlaybackBeforeMidroll_playsPreroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@Test
public void resumePlaybackAtMidroll_skipsPreroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void resumePlaybackAfterMidroll_skipsPreroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void resumePlaybackBeforeSecondMidroll_playsFirstMidroll() {
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long firstMidrollPeriodTimeUs = firstMidrollWindowTimeUs;
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
long secondMidrollPeriodTimeUs = secondMidrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@Test
public void resumePlaybackAtSecondMidroll_skipsFirstMidroll() {
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long firstMidrollPeriodTimeUs = firstMidrollWindowTimeUs;
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
long secondMidrollPeriodTimeUs = secondMidrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = (firstMidrollPeriodTimeUs + secondMidrollPeriodTimeUs) / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void resumePlaybackBeforeMidroll_withoutPlayAdBeforeStartPosition_skipsPreroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1d)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withSkippedAdGroup(/* adGroupIndex= */ 0)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@Test
public void resumePlaybackAtMidroll_withoutPlayAdBeforeStartPosition_skipsPreroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1d)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void resumePlaybackAfterMidroll_withoutPlayAdBeforeStartPosition_skipsMidroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdsManager).destroy();
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0)
.withSkippedAdGroup(/* adGroupIndex= */ 1));
}
@Test
public void
resumePlaybackBeforeSecondMidroll_withoutPlayAdBeforeStartPosition_skipsFirstMidroll() {
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long firstMidrollPeriodTimeUs = firstMidrollWindowTimeUs;
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
long secondMidrollPeriodTimeUs = secondMidrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = (firstMidrollPeriodTimeUs + secondMidrollPeriodTimeUs) / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1d)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withSkippedAdGroup(/* adGroupIndex= */ 0)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@Test
public void resumePlaybackAtSecondMidroll_withoutPlayAdBeforeStartPosition_skipsFirstMidroll() {
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long firstMidrollPeriodTimeUs = firstMidrollWindowTimeUs;
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
long secondMidrollPeriodTimeUs = secondMidrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = (firstMidrollPeriodTimeUs + secondMidrollPeriodTimeUs) / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1d)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void stop_unregistersAllVideoControlOverlays() { public void stop_unregistersAllVideoControlOverlays() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.requestAds(adViewGroup); imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.stop(); imaAdsLoader.stop();
...@@ -333,15 +614,71 @@ public final class ImaAdsLoaderTest { ...@@ -333,15 +614,71 @@ public final class ImaAdsLoaderTest {
inOrder.verify(mockAdDisplayContainer).unregisterAllVideoControlsOverlays(); inOrder.verify(mockAdDisplayContainer).unregisterAllVideoControlsOverlays();
} }
private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) { @Test
fakeExoPlayer = new FakePlayer(); public void loadAd_withLargeAdCuePoint_updatesAdPlaybackStateWithLoadedAd() {
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs); float midrollTimeSecs = 1_765f;
when(mockAdsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints)); ImmutableList<Float> cuePoints = ImmutableList.of(midrollTimeSecs);
imaAdsLoader = setupPlayback(CONTENT_TIMELINE, cuePoints);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
videoAdPlayer.loadAd(
TEST_AD_MEDIA_INFO,
new AdPodInfo() {
@Override
public int getTotalAds() {
return 1;
}
@Override
public int getAdPosition() {
return 1;
}
@Override
public boolean isBumper() {
return false;
}
@Override
public double getMaxDuration() {
return 0;
}
@Override
public int getPodIndex() {
return 0;
}
@Override
public double getTimeOffset() {
return midrollTimeSecs;
}
});
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI)
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}}));
}
private void setupPlayback(Timeline contentTimeline, List<Float> cuePoints) {
setupPlayback(
contentTimeline,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext()) new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setImaFactory(mockImaFactory) .setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings) .setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI); .buildForAdTag(TEST_URI));
}
private void setupPlayback(
Timeline contentTimeline, List<Float> cuePoints, ImaAdsLoader imaAdsLoader) {
fakeExoPlayer = new FakePlayer();
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline);
when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
this.imaAdsLoader = imaAdsLoader;
imaAdsLoader.setPlayer(fakeExoPlayer); imaAdsLoader.setPlayer(fakeExoPlayer);
} }
...@@ -349,9 +686,11 @@ public final class ImaAdsLoaderTest { ...@@ -349,9 +686,11 @@ public final class ImaAdsLoaderTest {
ArgumentCaptor<Object> userRequestContextCaptor = ArgumentCaptor.forClass(Object.class); ArgumentCaptor<Object> userRequestContextCaptor = ArgumentCaptor.forClass(Object.class);
doNothing().when(mockAdsRequest).setUserRequestContext(userRequestContextCaptor.capture()); doNothing().when(mockAdsRequest).setUserRequestContext(userRequestContextCaptor.capture());
when(mockAdsRequest.getUserRequestContext()) when(mockAdsRequest.getUserRequestContext())
.thenAnswer((Answer<Object>) invocation -> userRequestContextCaptor.getValue()); .thenAnswer(invocation -> userRequestContextCaptor.getValue());
List<com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener> adsLoadedListeners = List<com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener> adsLoadedListeners =
new ArrayList<>(); new ArrayList<>();
// Deliberately don't handle removeAdsLoadedListener to allow testing behavior if the IMA SDK
// invokes callbacks after release.
doAnswer( doAnswer(
invocation -> { invocation -> {
adsLoadedListeners.add(invocation.getArgument(0)); adsLoadedListeners.add(invocation.getArgument(0));
...@@ -359,13 +698,6 @@ public final class ImaAdsLoaderTest { ...@@ -359,13 +698,6 @@ public final class ImaAdsLoaderTest {
}) })
.when(mockAdsLoader) .when(mockAdsLoader)
.addAdsLoadedListener(any()); .addAdsLoadedListener(any());
doAnswer(
invocation -> {
adsLoadedListeners.remove(invocation.getArgument(0));
return null;
})
.when(mockAdsLoader)
.removeAdsLoadedListener(any());
when(mockAdsManagerLoadedEvent.getAdsManager()).thenReturn(mockAdsManager); when(mockAdsManagerLoadedEvent.getAdsManager()).thenReturn(mockAdsManager);
when(mockAdsManagerLoadedEvent.getUserRequestContext()) when(mockAdsManagerLoadedEvent.getUserRequestContext())
.thenAnswer(invocation -> mockAdsRequest.getUserRequestContext()); .thenAnswer(invocation -> mockAdsRequest.getUserRequestContext());
...@@ -381,6 +713,30 @@ public final class ImaAdsLoaderTest { ...@@ -381,6 +713,30 @@ public final class ImaAdsLoaderTest {
.when(mockAdsLoader) .when(mockAdsLoader)
.requestAds(mockAdsRequest); .requestAds(mockAdsRequest);
doAnswer(
invocation -> {
adEventListener = invocation.getArgument(0);
return null;
})
.when(mockAdsManager)
.addAdEventListener(any());
doAnswer(
invocation -> {
contentProgressProvider = invocation.getArgument(0);
return null;
})
.when(mockAdsRequest)
.setContentProgressProvider(any());
doAnswer(
invocation -> {
videoAdPlayer = invocation.getArgument(0);
return null;
})
.when(mockAdDisplayContainer)
.setPlayer(any());
when(mockImaFactory.createAdDisplayContainer()).thenReturn(mockAdDisplayContainer); when(mockImaFactory.createAdDisplayContainer()).thenReturn(mockAdDisplayContainer);
when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings); when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings);
when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest); when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest);
...@@ -422,19 +778,21 @@ public final class ImaAdsLoaderTest { ...@@ -422,19 +778,21 @@ public final class ImaAdsLoaderTest {
private final FakePlayer fakeExoPlayer; private final FakePlayer fakeExoPlayer;
private final Timeline contentTimeline; private final Timeline contentTimeline;
private final long[][] adDurationsUs;
public AdPlaybackState adPlaybackState; public AdPlaybackState adPlaybackState;
public TestAdsLoaderListener( public TestAdsLoaderListener(FakePlayer fakeExoPlayer, Timeline contentTimeline) {
FakePlayer fakeExoPlayer, Timeline contentTimeline, long[][] adDurationsUs) {
this.fakeExoPlayer = fakeExoPlayer; this.fakeExoPlayer = fakeExoPlayer;
this.contentTimeline = contentTimeline; this.contentTimeline = contentTimeline;
this.adDurationsUs = adDurationsUs;
} }
@Override @Override
public void onAdPlaybackState(AdPlaybackState adPlaybackState) { public void onAdPlaybackState(AdPlaybackState adPlaybackState) {
long[][] adDurationsUs = new long[adPlaybackState.adGroupCount][];
for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) {
adDurationsUs[adGroupIndex] = new long[adPlaybackState.adGroups[adGroupIndex].uris.length];
Arrays.fill(adDurationsUs[adGroupIndex], TEST_AD_DURATION_US);
}
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
this.adPlaybackState = adPlaybackState; this.adPlaybackState = adPlaybackState;
fakeExoPlayer.updateTimeline( fakeExoPlayer.updateTimeline(
......
...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.11.5"; public static final String VERSION = "2.11.6";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.5"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.6";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2011005; public static final int VERSION_INT = 2011006;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -124,13 +124,9 @@ public final class AdPlaybackState { ...@@ -124,13 +124,9 @@ public final class AdPlaybackState {
return result; return result;
} }
/** /** Returns a new instance with the ad count set to {@code count}. */
* Returns a new instance with the ad count set to {@code count}. This method may only be called
* if this instance's ad count has not yet been specified.
*/
@CheckResult @CheckResult
public AdGroup withAdCount(int count) { public AdGroup withAdCount(int count) {
Assertions.checkArgument(this.count == C.LENGTH_UNSET && states.length <= count);
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count); @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count);
long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count); long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count);
@NullableType Uri[] uris = Arrays.copyOf(this.uris, count); @NullableType Uri[] uris = Arrays.copyOf(this.uris, count);
...@@ -139,17 +135,11 @@ public final class AdPlaybackState { ...@@ -139,17 +135,11 @@ public final class AdPlaybackState {
/** /**
* Returns a new instance with the specified {@code uri} set for the specified ad, and the ad * Returns a new instance with the specified {@code uri} set for the specified ad, and the ad
* marked as {@link #AD_STATE_AVAILABLE}. The specified ad must currently be in {@link * marked as {@link #AD_STATE_AVAILABLE}.
* #AD_STATE_UNAVAILABLE}, which is the default state.
*
* <p>This instance's ad count may be unknown, in which case {@code index} must be less than the
* ad count specified later. Otherwise, {@code index} must be less than the current ad count.
*/ */
@CheckResult @CheckResult
public AdGroup withAdUri(Uri uri, int index) { public AdGroup withAdUri(Uri uri, int index) {
Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1); @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
Assertions.checkArgument(states[index] == AD_STATE_UNAVAILABLE);
long[] durationsUs = long[] durationsUs =
this.durationsUs.length == states.length this.durationsUs.length == states.length
? this.durationsUs ? this.durationsUs
...@@ -280,7 +270,9 @@ public final class AdPlaybackState { ...@@ -280,7 +270,9 @@ public final class AdPlaybackState {
public final AdGroup[] adGroups; public final AdGroup[] adGroups;
/** The position offset in the first unplayed ad at which to begin playback, in microseconds. */ /** The position offset in the first unplayed ad at which to begin playback, in microseconds. */
public final long adResumePositionUs; public final long adResumePositionUs;
/** The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. */ /**
* The duration of the content period in microseconds, if known. {@link C#TIME_UNSET} otherwise.
*/
public final long contentDurationUs; public final long contentDurationUs;
/** /**
...@@ -489,6 +481,54 @@ public final class AdPlaybackState { ...@@ -489,6 +481,54 @@ public final class AdPlaybackState {
return result; return result;
} }
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("AdPlaybackState(adResumePositionUs=");
sb.append(adResumePositionUs);
sb.append(", adGroups=[");
for (int i = 0; i < adGroups.length; i++) {
sb.append("adGroup(timeUs=");
sb.append(adGroupTimesUs[i]);
sb.append(", ads=[");
for (int j = 0; j < adGroups[i].states.length; j++) {
sb.append("ad(state=");
switch (adGroups[i].states[j]) {
case AD_STATE_UNAVAILABLE:
sb.append('_');
break;
case AD_STATE_ERROR:
sb.append('!');
break;
case AD_STATE_AVAILABLE:
sb.append('R');
break;
case AD_STATE_PLAYED:
sb.append('P');
break;
case AD_STATE_SKIPPED:
sb.append('S');
break;
default:
sb.append('?');
break;
}
sb.append(", durationUs=");
sb.append(adGroups[i].durationsUs[j]);
sb.append(')');
if (j < adGroups[i].states.length - 1) {
sb.append(", ");
}
}
sb.append("])");
if (i < adGroups.length - 1) {
sb.append(", ");
}
}
sb.append("])");
return sb.toString();
}
private boolean isPositionBeforeAdGroup( private boolean isPositionBeforeAdGroup(
long positionUs, long periodDurationUs, int adGroupIndex) { long positionUs, long periodDurationUs, int adGroupIndex) {
if (positionUs == C.TIME_END_OF_SOURCE) { if (positionUs == C.TIME_END_OF_SOURCE) {
......
...@@ -44,23 +44,16 @@ public final class SinglePeriodAdTimeline extends ForwardingTimeline { ...@@ -44,23 +44,16 @@ public final class SinglePeriodAdTimeline extends ForwardingTimeline {
@Override @Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) { public Period getPeriod(int periodIndex, Period period, boolean setIds) {
timeline.getPeriod(periodIndex, period, setIds); timeline.getPeriod(periodIndex, period, setIds);
long durationUs =
period.durationUs == C.TIME_UNSET ? adPlaybackState.contentDurationUs : period.durationUs;
period.set( period.set(
period.id, period.id,
period.uid, period.uid,
period.windowIndex, period.windowIndex,
period.durationUs, durationUs,
period.getPositionInWindowUs(), period.getPositionInWindowUs(),
adPlaybackState); adPlaybackState);
return period; return period;
} }
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
window = super.getWindow(windowIndex, window, defaultPositionProjectionUs);
if (window.durationUs == C.TIME_UNSET) {
window.durationUs = adPlaybackState.contentDurationUs;
}
return window;
}
} }
...@@ -155,12 +155,22 @@ public final class MimeTypes { ...@@ -155,12 +155,22 @@ public final class MimeTypes {
if (mimeType == null) { if (mimeType == null) {
return false; return false;
} }
// TODO: Consider adding additional audio MIME types here. // TODO: Add additional audio MIME types. Also consider evaluating based on Format rather than
// just MIME type, since in some cases the property is true for a subset of the profiles
// belonging to a single MIME type. If we do this, we should move the method to a different
// class. See [Internal ref: http://go/exo-audio-format-random-access].
switch (mimeType) { switch (mimeType) {
case AUDIO_AAC:
case AUDIO_MPEG: case AUDIO_MPEG:
case AUDIO_MPEG_L1: case AUDIO_MPEG_L1:
case AUDIO_MPEG_L2: case AUDIO_MPEG_L2:
case AUDIO_RAW:
case AUDIO_ALAW:
case AUDIO_MLAW:
case AUDIO_OPUS:
case AUDIO_FLAC:
case AUDIO_AC3:
case AUDIO_E_AC3:
case AUDIO_E_AC3_JOC:
return true; return true;
default: default:
return false; return false;
......
...@@ -48,6 +48,8 @@ import com.google.android.exoplayer2.ExoPlaybackException; ...@@ -48,6 +48,8 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.PictureFrame; import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame;
...@@ -1506,6 +1508,13 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider ...@@ -1506,6 +1508,13 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
SingleTapListener, SingleTapListener,
PlayerControlView.VisibilityListener { PlayerControlView.VisibilityListener {
private final Period period;
private @Nullable Object lastPeriodUidWithTracks;
public ComponentListener() {
period = new Period();
}
// TextOutput implementation // TextOutput implementation
@Override @Override
...@@ -1554,6 +1563,29 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider ...@@ -1554,6 +1563,29 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
@Override @Override
public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {
// Suppress the update if transitioning to an unprepared period within the same window. This
// is necessary to avoid closing the shutter when such a transition occurs. See:
// https://github.com/google/ExoPlayer/issues/5507.
Player player = Assertions.checkNotNull(PlayerView.this.player);
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
lastPeriodUidWithTracks = null;
} else if (!player.getCurrentTrackGroups().isEmpty()) {
lastPeriodUidWithTracks =
timeline.getPeriod(player.getCurrentPeriodIndex(), period, /* setIds= */ true).uid;
} else if (lastPeriodUidWithTracks != null) {
int lastPeriodIndexWithTracks = timeline.getIndexOfPeriod(lastPeriodUidWithTracks);
if (lastPeriodIndexWithTracks != C.INDEX_UNSET) {
int lastWindowIndexWithTracks =
timeline.getPeriod(lastPeriodIndexWithTracks, period).windowIndex;
if (player.getCurrentWindowIndex() == lastWindowIndexWithTracks) {
// We're in the same window. Suppress the update.
return;
}
}
lastPeriodUidWithTracks = null;
}
updateForCurrentTrackSelections(/* isNewPlayer= */ false); updateForCurrentTrackSelections(/* isNewPlayer= */ false);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment