diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a39e28a57f..60ce036861 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,9 @@ ### dev-v2 (not yet released) ### * Added dependency on checkerframework annotations for static code analysis. +* IMA: Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`, + and allow setting an ad event listener on `ImaAdsLoader`. Deprecate the + `AdsMediaSource.EventListener`. ### 2.8.0 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 60017a27e4..d3dbaaec96 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -52,6 +52,8 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState; import com.google.android.exoplayer2.source.ads.AdsLoader; +import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -80,6 +82,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final Context context; private @Nullable ImaSdkSettings imaSdkSettings; + private @Nullable AdEventListener adEventListener; private int vastLoadTimeoutMs; private int mediaLoadTimeoutMs; @@ -108,6 +111,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return this; } + /** + * Sets a listener for ad events that will be passed to {@link + * AdsManager#addAdEventListener(AdEventListener)}. + * + * @param adEventListener The ad event listener. + * @return This builder, for convenience. + */ + public Builder setAdEventListener(AdEventListener adEventListener) { + this.adEventListener = Assertions.checkNotNull(adEventListener); + return this; + } + /** * Sets the VAST load timeout, in milliseconds. * @@ -144,7 +159,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ public ImaAdsLoader buildForAdTag(Uri adTagUri) { return new ImaAdsLoader( - context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs, mediaLoadTimeoutMs); + context, + adTagUri, + imaSdkSettings, + null, + vastLoadTimeoutMs, + mediaLoadTimeoutMs, + adEventListener); } /** @@ -156,7 +177,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ public ImaAdsLoader buildForAdsResponse(String adsResponse) { return new ImaAdsLoader( - context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs, mediaLoadTimeoutMs); + context, + null, + imaSdkSettings, + adsResponse, + vastLoadTimeoutMs, + mediaLoadTimeoutMs, + adEventListener); } } @@ -214,6 +241,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final @Nullable String adsResponse; private final int vastLoadTimeoutMs; private final int mediaLoadTimeoutMs; + private final @Nullable AdEventListener adEventListener; private final Timeline.Period period; private final List adCallbacks; private final ImaSdkFactory imaSdkFactory; @@ -229,7 +257,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private VideoProgressUpdate lastAdProgress; private AdsManager adsManager; - private AdErrorEvent pendingAdErrorEvent; + private AdLoadException pendingAdLoadError; private Timeline timeline; private long contentDurationMs; private int podIndexOffset; @@ -308,7 +336,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A /* imaSdkSettings= */ null, /* adsResponse= */ null, /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET); + /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, + /* adEventListener= */ null); } /** @@ -330,7 +359,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A imaSdkSettings, /* adsResponse= */ null, /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET); + /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, + /* adEventListener= */ null); } private ImaAdsLoader( @@ -339,12 +369,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Nullable ImaSdkSettings imaSdkSettings, @Nullable String adsResponse, int vastLoadTimeoutMs, - int mediaLoadTimeoutMs) { + int mediaLoadTimeoutMs, + @Nullable AdEventListener adEventListener) { Assertions.checkArgument(adTagUri != null || adsResponse != null); this.adTagUri = adTagUri; this.adsResponse = adsResponse; this.vastLoadTimeoutMs = vastLoadTimeoutMs; this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; + this.adEventListener = adEventListener; period = new Timeline.Period(); adCallbacks = new ArrayList<>(1); imaSdkFactory = ImaSdkFactory.getInstance(); @@ -500,6 +532,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A 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 { @@ -544,13 +579,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A updateAdPlaybackState(); } else if (isAdGroupLoadError(error)) { try { - handleAdGroupLoadError(); + handleAdGroupLoadError(error); } catch (Exception e) { maybeNotifyInternalError("onAdError", e); } } - if (pendingAdErrorEvent == null) { - pendingAdErrorEvent = adErrorEvent; + if (pendingAdLoadError == null) { + pendingAdLoadError = AdLoadException.createForAllAds(error); } maybeNotifyPendingAdLoadError(); } @@ -937,9 +972,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A break; case LOG: Map adData = adEvent.getAdData(); - Log.i(TAG, "Log AdEvent: " + adData); + String message = "AdEvent: " + adData; + Log.i(TAG, message); if ("adLoadError".equals(adData.get("type"))) { - handleAdGroupLoadError(); + handleAdGroupLoadError(new IOException(message)); } break; case ALL_ADS_COMPLETED: @@ -1011,7 +1047,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } - private void handleAdGroupLoadError() { + private void handleAdGroupLoadError(Exception error) { int adGroupIndex = this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; if (adGroupIndex == C.INDEX_UNSET) { @@ -1033,6 +1069,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } updateAdPlaybackState(); + if (pendingAdLoadError == null) { + pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); + } } private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { @@ -1111,21 +1150,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } private void maybeNotifyPendingAdLoadError() { - if (pendingAdErrorEvent != null) { - if (eventListener != null) { - eventListener.onAdLoadError( - new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError())); - } - pendingAdErrorEvent = null; + if (pendingAdLoadError != null && eventListener != null) { + eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri)); + pendingAdLoadError = null; } } private void maybeNotifyInternalError(String name, Exception cause) { String message = "Internal error in " + name; Log.e(TAG, message, cause); - if (eventListener != null) { - eventListener.onInternalAdLoadError(new RuntimeException(message, cause)); - } // We can't recover from an unexpected error in general, so skip all remaining ads. if (adPlaybackState == null) { adPlaybackState = new AdPlaybackState(); @@ -1135,6 +1168,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } updateAdPlaybackState(); + if (eventListener != null) { + eventListener.onAdLoadError( + AdLoadException.createForUnexpected(new RuntimeException(message, cause)), + new DataSpec(adTagUri)); + } } private static long[] getAdGroupTimesUs(List cuePoints) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 79de146f29..de210f5eff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -493,6 +493,8 @@ public final class C { * A data type constant for time synchronization data. */ public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5; + /** A data type constant for ads loader data. */ + public static final int DATA_TYPE_AD = 6; /** * Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or * equal to this value. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java index ab6b3a311a..229043b127 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -37,7 +37,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb /** * Called the first time an error occurs while refreshing source info or preparing the period. */ - void onPrepareError(IOException exception); + void onPrepareError(MediaPeriodId mediaPeriodId, IOException exception); } public final MediaSource mediaSource; @@ -140,7 +140,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb } if (!notifiedPrepareError) { notifiedPrepareError = true; - listener.onPrepareError(e); + listener.onPrepareError(id, e); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java index 6295ca4229..d05c51a793 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.source.ads; import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; +import com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; /** @@ -54,19 +56,12 @@ public interface AdsLoader { void onAdPlaybackState(AdPlaybackState adPlaybackState); /** - * Called when there was an error loading ads. The loader will skip the problematic ad(s). + * Called when there was an error loading ads. * * @param error The error. + * @param dataSpec The data spec associated with the load error. */ - void onAdLoadError(IOException error); - - /** - * Called when an unexpected internal error is encountered while loading ads. The loader will - * skip all remaining ads, as the error is not recoverable. - * - * @param error The error. - */ - void onInternalAdLoadError(RuntimeException error); + void onAdLoadError(AdLoadException error, DataSpec dataSpec); /** * Called when the user clicks through an ad (for example, following a 'learn more' link). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 9e93f8d92d..7f9dc18eaf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -18,8 +18,8 @@ package com.google.android.exoplayer2.source.ads; import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; -import android.util.Log; import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; @@ -30,10 +30,16 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -64,7 +70,75 @@ public final class AdsMediaSource extends CompositeMediaSource { int[] getSupportedTypes(); } - /** Listener for ads media source events. */ + /** + * Wrapper for exceptions that occur while loading ads, which are notified via {@link + * MediaSourceEventListener#onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, + * IOException, boolean)}. + */ + public static final class AdLoadException extends IOException { + + /** Types of ad load exceptions. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_AD, TYPE_AD_GROUP, TYPE_ALL_ADS, TYPE_UNEXPECTED}) + public @interface Type {} + /** Type for when an ad failed to load. The ad will be skipped. */ + public static final int TYPE_AD = 0; + /** Type for when an ad group failed to load. The ad group will be skipped. */ + public static final int TYPE_AD_GROUP = 1; + /** Type for when all ad groups failed to load. All ads will be skipped. */ + public static final int TYPE_ALL_ADS = 2; + /** Type for when an unexpected error occurred while loading ads. All ads will be skipped. */ + public static final int TYPE_UNEXPECTED = 3; + + /** Returns a new ad load exception of {@link #TYPE_AD}. */ + public static AdLoadException createForAd(Exception error) { + return new AdLoadException(TYPE_AD, error); + } + + /** Returns a new ad load exception of {@link #TYPE_AD_GROUP}. */ + public static AdLoadException createForAdGroup(Exception error, int adGroupIndex) { + return new AdLoadException( + TYPE_AD_GROUP, new IOException("Failed to load ad group " + adGroupIndex, error)); + } + + /** Returns a new ad load exception of {@link #TYPE_ALL_ADS}. */ + public static AdLoadException createForAllAds(Exception error) { + return new AdLoadException(TYPE_ALL_ADS, error); + } + + /** Returns a new ad load exception of {@link #TYPE_UNEXPECTED}. */ + public static AdLoadException createForUnexpected(RuntimeException error) { + return new AdLoadException(TYPE_UNEXPECTED, error); + } + + /** The {@link Type} of the ad load exception. */ + public final @Type int type; + + private AdLoadException(@Type int type, Exception cause) { + super(cause); + this.type = type; + } + + /** + * Returns the {@link RuntimeException} that caused the exception if its type is {@link + * #TYPE_UNEXPECTED}. + */ + public RuntimeException getRuntimeExceptionForUnexpected() { + Assertions.checkState(type == TYPE_UNEXPECTED); + return (RuntimeException) getCause(); + } + } + + /** + * Listener for ads media source events. + * + * @deprecated To listen for ad load error events, add a listener via {@link + * #addEventListener(Handler, MediaSourceEventListener)} and check for {@link + * AdLoadException}s in {@link MediaSourceEventListener#onLoadError(int, MediaPeriodId, + * LoadEventInfo, MediaLoadData, IOException, boolean)}. Individual ads loader implementations + * should expose ad interaction events, if applicable. + */ + @Deprecated public interface EventListener { /** @@ -131,7 +205,30 @@ public final class AdsMediaSource extends CompositeMediaSource { ViewGroup adUiViewGroup) { this( contentMediaSource, - dataSourceFactory, + new ExtractorMediaSource.Factory(dataSourceFactory), + adsLoader, + adUiViewGroup, + /* eventHandler= */ null, + /* eventListener= */ null); + } + + /** + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param adMediaSourceFactory Factory for media sources used to load ad media. + * @param adsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + public AdsMediaSource( + MediaSource contentMediaSource, + MediaSourceFactory adMediaSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup) { + this( + contentMediaSource, + adMediaSourceFactory, adsLoader, adUiViewGroup, /* eventHandler= */ null, @@ -148,7 +245,13 @@ public final class AdsMediaSource extends CompositeMediaSource { * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated To listen for ad load error events, add a listener via {@link + * #addEventListener(Handler, MediaSourceEventListener)} and check for {@link + * AdLoadException}s in {@link MediaSourceEventListener#onLoadError(int, MediaPeriodId, + * LoadEventInfo, MediaLoadData, IOException, boolean)}. Individual ads loader implementations + * should expose ad interaction events, if applicable. */ + @Deprecated public AdsMediaSource( MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, @@ -175,7 +278,13 @@ public final class AdsMediaSource extends CompositeMediaSource { * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated To listen for ad load error events, add a listener via {@link + * #addEventListener(Handler, MediaSourceEventListener)} and check for {@link + * AdLoadException}s in {@link MediaSourceEventListener#onLoadError(int, MediaPeriodId, + * LoadEventInfo, MediaLoadData, IOException, boolean)}. Individual ads loader implementations + * should expose ad interaction events, if applicable. */ + @Deprecated public AdsMediaSource( MediaSource contentMediaSource, MediaSourceFactory adMediaSourceFactory, @@ -217,10 +326,10 @@ public final class AdsMediaSource extends CompositeMediaSource { if (adPlaybackState.adGroupCount > 0 && id.isAd()) { int adGroupIndex = id.adGroupIndex; int adIndexInAdGroup = id.adIndexInAdGroup; + Uri adUri = adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]; if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - Uri adUri = adPlaybackState.adGroups[id.adGroupIndex].uris[id.adIndexInAdGroup]; MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adUri); - int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; + int oldAdCount = adGroupMediaSources[adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; adGroupMediaSources[adGroupIndex] = @@ -239,7 +348,7 @@ public final class AdsMediaSource extends CompositeMediaSource { new MediaPeriodId(/* periodIndex= */ 0, id.windowSequenceNumber), allocator); deferredMediaPeriod.setPrepareErrorListener( - new AdPrepareErrorListener(adGroupIndex, adIndexInAdGroup)); + new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); if (mediaPeriods == null) { deferredMediaPeriod.createPeriod(); @@ -357,6 +466,7 @@ public final class AdsMediaSource extends CompositeMediaSource { private final class ComponentListener implements AdsLoader.EventListener { private final Handler playerHandler; + private volatile boolean released; /** @@ -424,37 +534,30 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void onAdLoadError(final IOException error) { + public void onAdLoadError(final AdLoadException error, DataSpec dataSpec) { if (released) { return; } - Log.w(TAG, "Ad load error", error); + createEventDispatcher(/* mediaPeriodId= */ null) + .loadError( + dataSpec, + C.DATA_TYPE_AD, + C.TRACK_TYPE_UNKNOWN, + /* loadDurationMs= */ 0, + /* bytesLoaded= */ 0, + error, + /* wasCanceled= */ true); if (eventHandler != null && eventListener != null) { eventHandler.post( new Runnable() { @Override public void run() { if (!released) { - eventListener.onAdLoadError(error); - } - } - }); - } - } - - @Override - public void onInternalAdLoadError(final RuntimeException error) { - if (released) { - return; - } - Log.w(TAG, "Internal ad load error", error); - if (eventHandler != null && eventListener != null) { - eventHandler.post( - new Runnable() { - @Override - public void run() { - if (!released) { - eventListener.onInternalAdLoadError(error); + if (error.type == AdLoadException.TYPE_UNEXPECTED) { + eventListener.onInternalAdLoadError(error.getRuntimeExceptionForUnexpected()); + } else { + eventListener.onAdLoadError(error); + } } } }); @@ -464,16 +567,27 @@ public final class AdsMediaSource extends CompositeMediaSource { private final class AdPrepareErrorListener implements DeferredMediaPeriod.PrepareErrorListener { + private final Uri adUri; private final int adGroupIndex; private final int adIndexInAdGroup; - public AdPrepareErrorListener(int adGroupIndex, int adIndexInAdGroup) { + public AdPrepareErrorListener(Uri adUri, int adGroupIndex, int adIndexInAdGroup) { + this.adUri = adUri; this.adGroupIndex = adGroupIndex; this.adIndexInAdGroup = adIndexInAdGroup; } @Override - public void onPrepareError(final IOException exception) { + public void onPrepareError(MediaPeriodId mediaPeriodId, final IOException exception) { + createEventDispatcher(mediaPeriodId) + .loadError( + new DataSpec(adUri), + C.DATA_TYPE_AD, + C.TRACK_TYPE_UNKNOWN, + /* loadDurationMs= */ 0, + /* bytesLoaded= */ 0, + AdLoadException.createForAd(exception), + /* wasCanceled= */ true); mainHandler.post( new Runnable() { @Override