Enable nullness checking for the IMA extension

adPlaybackState is now non-null, and the uninitialized case is covered by a new
boolean hasAdPlaybackState. Position progress updates are now non-null and
initialized with IMA's VIDEO_TIME_NOT_READY constant.

Also fix some misc code issues:
- Remove empty branch for SmoothStreaming (Android Studio warns about this).
- Tidy onTimelineChanged and onPositionDiscontinuity and the methods they call
  to improve naming.
- Remove logging for IMA events after release, as these methods are expected to
  be called in the current IMA SDK behavior.

PiperOrigin-RevId: 308977116
This commit is contained in:
andrewlewis 2020-04-29 08:52:01 +01:00 committed by Oliver Woodman
parent dcbdbe5341
commit 625c734726
2 changed files with 68 additions and 56 deletions

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
@ -282,7 +284,7 @@ public final class ImaAdsLoader
* Threshold before the end of content at which IMA is notified that content is complete if the
* player buffers, in milliseconds.
*/
private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000;
private static final long END_OF_CONTENT_THRESHOLD_MS = 5000;
/** The maximum duration before an ad break that IMA may start preloading the next ad. */
private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000;
@ -324,7 +326,7 @@ public final class ImaAdsLoader
private boolean wasSetPlayerCalled;
@Nullable private Player nextPlayer;
private Object pendingAdRequestContext;
@Nullable private Object pendingAdRequestContext;
private List<String> supportedMimeTypes;
@Nullable private EventListener eventListener;
@Nullable private Player player;
@ -334,7 +336,8 @@ public final class ImaAdsLoader
@Nullable private AdsManager adsManager;
private boolean initializedAdsManager;
private AdLoadException pendingAdLoadError;
private boolean hasAdPlaybackState;
@Nullable private AdLoadException pendingAdLoadError;
private Timeline timeline;
private long contentDurationMs;
private int podIndexOffset;
@ -438,6 +441,7 @@ public final class ImaAdsLoader
/* imaFactory= */ new DefaultImaFactory());
}
@SuppressWarnings("nullness:argument.type.incompatible")
private ImaAdsLoader(
Context context,
@Nullable Uri adTagUri,
@ -477,12 +481,16 @@ public final class ImaAdsLoader
context.getApplicationContext(), imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(/* adErrorListener= */ this);
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
supportedMimeTypes = Collections.emptyList();
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
fakeContentProgressOffsetMs = C.TIME_UNSET;
pendingContentPositionMs = C.TIME_UNSET;
adGroupIndex = C.INDEX_UNSET;
contentDurationMs = C.TIME_UNSET;
timeline = Timeline.EMPTY;
adPlaybackState = AdPlaybackState.NONE;
}
/**
@ -530,22 +538,22 @@ public final class ImaAdsLoader
* @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
*/
public void requestAds(ViewGroup adViewGroup) {
if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) {
if (hasAdPlaybackState || adsManager != null || pendingAdRequestContext != null) {
// Ads have already been requested.
return;
}
adDisplayContainer.setAdContainer(adViewGroup);
pendingAdRequestContext = new Object();
AdsRequest request = imaFactory.createAdsRequest();
if (adTagUri != null) {
request.setAdTagUrl(adTagUri.toString());
} else /* adsResponse != null */ {
request.setAdsResponse(adsResponse);
} else {
request.setAdsResponse(castNonNull(adsResponse));
}
if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
request.setVastLoadTimeout(vastLoadTimeoutMs);
}
request.setContentProgressProvider(this);
pendingAdRequestContext = new Object();
request.setUserRequestContext(pendingAdRequestContext);
adsLoader.requestAds(request);
}
@ -565,6 +573,7 @@ public final class ImaAdsLoader
public void setSupportedContentTypes(@C.ContentType int... contentTypes) {
List<String> supportedMimeTypes = new ArrayList<>();
for (@C.ContentType int contentType : contentTypes) {
// IMA does not support Smooth Streaming ad media.
if (contentType == C.TYPE_DASH) {
supportedMimeTypes.add(MimeTypes.APPLICATION_MPD);
} else if (contentType == C.TYPE_HLS) {
@ -577,8 +586,6 @@ public final class ImaAdsLoader
MimeTypes.VIDEO_H263,
MimeTypes.AUDIO_MP4,
MimeTypes.AUDIO_MPEG));
} else if (contentType == C.TYPE_SS) {
// IMA does not support Smooth Streaming ad media.
}
}
this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);
@ -592,22 +599,23 @@ public final class ImaAdsLoader
if (player == null) {
return;
}
player.addListener(this);
boolean playWhenReady = player.getPlayWhenReady();
this.eventListener = eventListener;
lastVolumePercentage = 0;
lastAdProgress = null;
lastContentProgress = null;
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
ViewGroup adViewGroup = adViewProvider.getAdViewGroup();
adDisplayContainer.setAdContainer(adViewGroup);
View[] adOverlayViews = adViewProvider.getAdOverlayViews();
for (View view : adOverlayViews) {
adDisplayContainer.registerVideoControlsOverlay(view);
}
player.addListener(this);
maybeNotifyPendingAdLoadError();
if (adPlaybackState != null) {
if (hasAdPlaybackState) {
// Pass the ad playback state to the player, and resume ads if necessary.
eventListener.onAdPlaybackState(adPlaybackState);
if (imaPausedContent && player.getPlayWhenReady()) {
if (adsManager != null && imaPausedContent && playWhenReady) {
adsManager.resume();
}
} else if (adsManager != null) {
@ -621,21 +629,22 @@ public final class ImaAdsLoader
@Override
public void stop() {
@Nullable Player player = this.player;
if (player == null) {
return;
}
if (adsManager != null && imaPausedContent) {
adsManager.pause();
adPlaybackState =
adPlaybackState.withAdResumePositionUs(
playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
adsManager.pause();
}
lastVolumePercentage = getVolume();
lastAdProgress = getAdProgress();
lastContentProgress = getContentProgress();
adDisplayContainer.unregisterAllVideoControlsOverlays();
player.removeListener(this);
player = null;
this.player = null;
eventListener = null;
}
@ -657,6 +666,7 @@ public final class ImaAdsLoader
imaAdState = IMA_AD_STATE_NONE;
pendingAdLoadError = null;
adPlaybackState = AdPlaybackState.NONE;
hasAdPlaybackState = false;
updateAdPlaybackState();
}
@ -692,6 +702,7 @@ public final class ImaAdsLoader
// 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);
@ -708,7 +719,7 @@ public final class ImaAdsLoader
Log.d(TAG, "onAdEvent: " + adEventType);
}
if (adsManager == null) {
Log.w(TAG, "Ignoring AdEvent after release: " + adEvent);
// Drop events after release.
return;
}
try {
@ -729,7 +740,8 @@ public final class ImaAdsLoader
if (adsManager == null) {
// No ads were loaded, so allow playback to start without any ads.
pendingAdRequestContext = null;
adPlaybackState = new AdPlaybackState();
adPlaybackState = AdPlaybackState.NONE;
hasAdPlaybackState = true;
updateAdPlaybackState();
} else if (isAdGroupLoadError(error)) {
try {
@ -766,7 +778,7 @@ public final class ImaAdsLoader
adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
} else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
contentPositionMs = getContentPeriodPositionMs();
contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
// Update the expected ad group index for the current content position. The update is delayed
// until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered
// just after an ad group isn't incorrectly attributed to the next ad group.
@ -806,11 +818,12 @@ public final class ImaAdsLoader
@Override
public int getVolume() {
@Nullable Player player = this.player;
if (player == null) {
return lastVolumePercentage;
}
Player.AudioComponent audioComponent = player.getAudioComponent();
@Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
if (audioComponent != null) {
return (int) (audioComponent.getVolume() * 100);
}
@ -832,16 +845,16 @@ public final class ImaAdsLoader
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
}
if (adsManager == null) {
Log.w(TAG, "Ignoring loadAd after release");
// Drop events after release.
return;
}
if (adGroupIndex == C.INDEX_UNSET) {
adGroupIndex = expectedAdGroupIndex;
adsManager.start();
Log.w(
TAG,
"Unexpected loadAd without LOADED event; assuming ad group index is actually "
+ expectedAdGroupIndex);
adGroupIndex = expectedAdGroupIndex;
adsManager.start();
}
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
if (adIndexInAdGroup == C.INDEX_UNSET) {
@ -872,7 +885,7 @@ public final class ImaAdsLoader
Log.d(TAG, "playAd");
}
if (adsManager == null) {
Log.w(TAG, "Ignoring playAd after release");
// Drop events after release.
return;
}
switch (imaAdState) {
@ -909,7 +922,7 @@ public final class ImaAdsLoader
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
Log.w(TAG, "Unexpected playAd while detached");
} else if (!player.getPlayWhenReady()) {
adsManager.pause();
Assertions.checkNotNull(adsManager).pause();
}
}
@ -919,7 +932,7 @@ public final class ImaAdsLoader
Log.d(TAG, "stopAd");
}
if (adsManager == null) {
Log.w(TAG, "Ignoring stopAd after release");
// Drop event after release.
return;
}
if (player == null) {
@ -975,9 +988,14 @@ public final class ImaAdsLoader
}
if (!initializedAdsManager && adsManager != null) {
initializedAdsManager = true;
initializeAdsManager();
initializeAdsManager(adsManager);
}
checkForContentCompleteOrNewAdGroup();
handleTimelineOrPositionChanged();
}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
handleTimelineOrPositionChanged();
}
@Override
@ -1016,14 +1034,9 @@ public final class ImaAdsLoader
}
}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
checkForContentCompleteOrNewAdGroup();
}
// Internal methods.
private void initializeAdsManager() {
private void initializeAdsManager(AdsManager adsManager) {
AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
adsRenderingSettings.setEnablePreloading(true);
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
@ -1040,7 +1053,8 @@ public final class ImaAdsLoader
// Skip ads based on the start position as required.
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
long contentPositionMs = getContentPeriodPositionMs();
long contentPositionMs =
getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period);
int adGroupIndexForPosition =
adPlaybackState.getAdGroupIndexForPositionUs(
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
@ -1093,7 +1107,7 @@ public final class ImaAdsLoader
podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);
int adPosition = adPodInfo.getAdPosition();
int adCount = adPodInfo.getTotalAds();
adsManager.start();
Assertions.checkNotNull(adsManager).start();
if (DEBUG) {
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex);
}
@ -1145,8 +1159,6 @@ public final class ImaAdsLoader
handleAdGroupLoadError(new IOException(message));
}
break;
case STARTED:
case ALL_ADS_COMPLETED:
default:
break;
}
@ -1167,7 +1179,8 @@ public final class ImaAdsLoader
}
}
private void checkForContentCompleteOrNewAdGroup() {
private void handleTimelineOrPositionChanged() {
@Nullable Player player = this.player;
if (adsManager == null || player == null) {
return;
}
@ -1181,7 +1194,7 @@ public final class ImaAdsLoader
}
updateAdPlaybackState();
} else if (!timeline.isEmpty()) {
long positionMs = getContentPeriodPositionMs();
long positionMs = getContentPeriodPositionMs(player, timeline, period);
timeline.getPeriod(/* periodIndex= */ 0, period);
int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs));
if (newAdGroupIndex != C.INDEX_UNSET) {
@ -1193,10 +1206,7 @@ public final class ImaAdsLoader
}
}
}
updateImaStateForPlayerState();
}
private void updateImaStateForPlayerState() {
boolean wasPlayingAd = playingAd;
int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup;
playingAd = player.isPlayingAd();
@ -1323,10 +1333,11 @@ public final class ImaAdsLoader
}
private void checkForContentComplete() {
if (contentDurationMs != C.TIME_UNSET
long positionMs = getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period);
if (!sentContentComplete
&& contentDurationMs != C.TIME_UNSET
&& pendingContentPositionMs == C.TIME_UNSET
&& getContentPeriodPositionMs() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
&& !sentContentComplete) {
&& positionMs + END_OF_CONTENT_THRESHOLD_MS >= contentDurationMs) {
adsLoader.contentComplete();
if (DEBUG) {
Log.d(TAG, "adsLoader.contentComplete");
@ -1364,7 +1375,7 @@ public final class ImaAdsLoader
private void maybeNotifyPendingAdLoadError() {
if (pendingAdLoadError != null && eventListener != null) {
eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri));
eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri));
pendingAdLoadError = null;
}
}
@ -1373,22 +1384,23 @@ public final class ImaAdsLoader
String message = "Internal error in " + name;
Log.e(TAG, message, cause);
// We can't recover from an unexpected error in general, so skip all remaining ads.
if (adPlaybackState == null) {
adPlaybackState = AdPlaybackState.NONE;
} else {
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
updateAdPlaybackState();
if (eventListener != null) {
eventListener.onAdLoadError(
AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
new DataSpec(adTagUri));
getAdsDataSpec(adTagUri));
}
}
private long getContentPeriodPositionMs() {
private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) {
return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY);
}
private static long getContentPeriodPositionMs(
Player player, Timeline timeline, Timeline.Period period) {
long contentWindowPositionMs = player.getContentPosition();
return contentWindowPositionMs
- timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs();

View file

@ -271,7 +271,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
}
@Override
protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
MediaPeriodId childId, MediaPeriodId mediaPeriodId) {
// The child id for the content period is just DUMMY_CONTENT_MEDIA_PERIOD_ID. That's why we need
// to forward the reported mediaPeriodId in this case.