diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 522d2f457eb..f5353a24e1c 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -1,5 +1,277 @@
# Release notes
+### 2.13.0 (2021-02-04)
+
+* Core library:
+ * Verify correct thread usage in `SimpleExoPlayer` by default. Opt-out is
+ still possible until the next major release using
+ `setThrowsWhenUsingWrongThread(false)`
+ ([#4463](https://github.com/google/ExoPlayer/issues/4463)).
+ * Add `Player.getCurrentStaticMetadata` and
+ `EventListener.onStaticMetadataChanged` to expose static metadata
+ belonging to the currently playing stream
+ ([#7266](https://github.com/google/ExoPlayer/issues/7266)).
+ * Add `PlayerMessage.setLooper` and deprecate `PlayerMessage.setHandler`.
+ * Add option to `MergingMediaSource` to clip the durations of all sources
+ to have the same length
+ ([#8422](https://github.com/google/ExoPlayer/issues/8422)).
+ * Remove `Player.setVideoDecoderOutputBufferRenderer` from Player API. Use
+ `setVideoSurfaceView` and `clearVideoSurfaceView` instead.
+ * Default `SingleSampleMediaSource.treatLoadErrorsAsEndOfStream` to `true`
+ so that errors loading external subtitle files do not cause playback
+ to fail ([#8430](https://github.com/google/ExoPlayer/issues/8430)). A
+ warning will be logged by `SingleSampleMediaPeriod` whenever a load
+ error is treated as though the end of the stream has been reached.
+ * Time out on release to prevent ANRs if an underlying platform call is
+ stuck ([#4352](https://github.com/google/ExoPlayer/issues/4352)).
+ * Time out when detaching a surface to prevent ANRs if the underlying
+ platform call is stuck
+ ([#5887](https://github.com/google/ExoPlayer/issues/5887)).
+ * Fix bug where `AnalyticsListener` callbacks could arrive in the wrong
+ order ([#8048](https://github.com/google/ExoPlayer/issues/8048)).
+* Media transformation:
+ * Add a new `transformer` module for converting media streams. The
+ initially available transformations are changing the container format,
+ removing tracks, and slow motion flattening.
+* Low latency live streaming:
+ * Support low-latency DASH (also known as ULL-CMAF) and Apple's
+ low-latency HLS extension.
+ * Add `LiveConfiguration` to `MediaItem` to define live offset and
+ playback speed adjustment parameters. The same parameters can be set on
+ `DefaultMediaSourceFactory` to apply for all `MediaItems`.
+ * Add `LivePlaybackSpeedControl` to control playback speed adjustments
+ during live playbacks. Such adjustments allow the player to stay close
+ to the live offset. `DefaultLivePlaybackSpeedControl` is provided as a
+ default implementation.
+ * Add `targetLiveOffsetUs` parameter to `LoadControl.shouldStartPlayback`.
+* Extractors:
+ * Populate codecs string for H.264/AVC in MP4, Matroska and FLV streams to
+ allow decoder capability checks based on codec profile and level
+ ([#8393](https://github.com/google/ExoPlayer/issues/8393)).
+ * Populate codecs string for H.265/HEVC in MP4, Matroska and MPEG-TS
+ streams to allow decoder capability checks based on codec profile and
+ level ([#8393](https://github.com/google/ExoPlayer/issues/8393)).
+ * Add support for playing JPEG motion photos
+ ([#5405](https://github.com/google/ExoPlayer/issues/5405)).
+ * Handle sample size mismatches between raw audio `stsd` information and
+ `stsz` fixed sample size in MP4 extractors.
+ * Fix Vorbis private codec data parsing in the Matroska extractor
+ ([#8496](https://github.com/google/ExoPlayer/issues/8496)).
+* Track selection:
+ * Move `Player.getTrackSelector` to the `ExoPlayer` interface.
+ * Move the mutable parts of `TrackSelection` into an `ExoTrackSelection`
+ subclass.
+ * Allow parallel adaptation of video and audio
+ ([#5111](https://github.com/google/ExoPlayer/issues/5111)).
+ * Simplify enabling tunneling with `DefaultTrackSelector`.
+ `ParametersBuilder.setTunnelingAudioSessionId` has been replaced with
+ `ParametersBuilder.setTunnelingEnabled`. The player's audio session ID
+ will be used, and so a tunneling specific ID is no longer needed.
+ * Add additional configuration parameters to `DefaultTrackSelector`.
+ `DefaultTrackSelector.ParametersBuilder` now includes:
+ * `setPreferredVideoMimeType`, `setPreferredVideoMimeTypes`,
+ `setPreferredAudioMimeType` and `setPreferredAudioMimeTypes` for
+ specifying preferred video and audio MIME type(s)
+ ([#8320](https://github.com/google/ExoPlayer/issues/8320)).
+ * `setPreferredAudioLanguages` and `setPreferredTextLanguages` for
+ specifying multiple preferred audio and text languages.
+ * `setPreferredAudioRoleFlags` for specifying preferred audio role
+ flags.
+ * Forward `Timeline` and `MediaPeriodId` to `TrackSelection.Factory`.
+* DASH:
+ * Support low-latency DASH playback (`availabilityTimeOffset` and
+ `ServiceDescription` tags)
+ ([#4904](https://github.com/google/ExoPlayer/issues/4904)).
+ * Improve logic for determining whether to refresh the manifest when a
+ chunk load error occurs in a live streams that contains EMSG data
+ ([#8408](https://github.com/google/ExoPlayer/issues/8408)).
+* HLS:
+ * Support playlist delta updates, blocking playlist reloads and rendition
+ reports.
+ * Support low-latency HLS playback (`EXT-X-PART` and preload hints)
+ ([#5011](https://github.com/google/ExoPlayer/issues/5011)).
+* UI:
+ * Improve `StyledPlayerControlView` button animations.
+ * Miscellaneous fixes for `StyledPlayerControlView` in minimal mode.
+* DRM:
+ * Fix playback failure when switching from PlayReady protected content to
+ Widevine or Clearkey protected content in a playlist.
+ * Add `ExoMediaDrm.KeyRequest.getRequestType`
+ ([#7847](https://github.com/google/ExoPlayer/issues/7847)).
+ * Drop key and provision responses if `DefaultDrmSession` is released
+ while waiting for the response. This prevents harmless log messages of
+ the form:
+ `IllegalStateException: sending message to a Handler on a dead thread`
+ ([#8328](https://github.com/google/ExoPlayer/issues/8328)).
+ * Allow apps to fully customize DRM behaviour for each `MediaItem` by
+ passing a `DrmSessionManagerProvider` to `MediaSourceFactory`
+ ([#8466](https://github.com/google/ExoPlayer/issues/8466)).
+* Analytics:
+ * Add an `onEvents` callback to `Player.EventListener` and
+ `AnalyticsListener`. When one or more player states change
+ simultaneously, `onEvents` is called once after all of the callbacks
+ associated with the individual state changes.
+ * Pass a `DecoderReuseEvaluation` to `AnalyticsListener`'s
+ `onVideoInputFormatChanged` and `onAudioInputFormatChanged` methods. The
+ `DecoderReuseEvaluation` indicates whether it was possible to re-use an
+ existing decoder instance for the new format, and if not then the
+ reasons why.
+* Video:
+ * Fall back to AVC/HEVC decoders for Dolby Vision streams with level 10
+ to 13 ([#8530](https://github.com/google/ExoPlayer/issues/8530)).
+ * Fix VP9 format capability checks on API level 23 and earlier. The
+ platform does not correctly report the VP9 level supported by the
+ decoder in this case, so we estimate it based on the decoder's maximum
+ supported bitrate.
+* Audio:
+ * Fix handling of audio session IDs
+ ([#8190](https://github.com/google/ExoPlayer/issues/8190)).
+ `SimpleExoPlayer` now generates an audio session ID on construction,
+ which can be immediately queried by calling
+ `SimpleExoPlayer.getAudioSessionId`. The audio session ID will only
+ change if application code calls `SimpleExoPlayer.setAudioSessionId`.
+ * Replace `onAudioSessionId` with `onAudioSessionIdChanged` in
+ `AudioListener` and `AnalyticsListener`. Note that
+ `onAudioSessionIdChanged` is called in fewer cases than
+ `onAudioSessionId` was called, due to the improved handling of audio
+ session IDs as described above.
+ * Retry playback after some types of `AudioTrack` error.
+ * Create E-AC3 JOC passthrough `AudioTrack` instances using the maximum
+ supported channel count (instead of assuming 6 channels) from API 29.
+* Text:
+ * Add support for the SSA `primaryColour` style attribute
+ ([#8435](https://github.com/google/ExoPlayer/issues/8435)).
+ * Fix CEA-708 sequence number discontinuity handling
+ ([#1807](https://github.com/google/ExoPlayer/issues/1807)).
+ * Fix CEA-708 handling of unexpectedly small packets
+ ([#1807](https://github.com/google/ExoPlayer/issues/1807)).
+* Data sources:
+ * For `HttpDataSource` implementations, default to using the user agent of
+ the underlying network stack.
+ * Deprecate `HttpDataSource.Factory.getDefaultRequestProperties`.
+ `HttpDataSource.Factory.setDefaultRequestProperties` instead.
+ * Add `DefaultHttpDataSource.Factory` and deprecate
+ `DefaultHttpDataSourceFactory`.
+* Metadata retriever:
+ * Parse Google Photos HEIC and JPEG motion photo metadata.
+* IMA extension:
+ * Add support for playback of ads in playlists
+ ([#3750](https://github.com/google/ExoPlayer/issues/3750)).
+ * Add `ImaAdsLoader.Builder.setEnableContinuousPlayback` for setting
+ whether to request ads for continuous playback.
+ * Upgrade IMA SDK dependency to 3.22.0. This fixes leaking of the ad view
+ group ([#7344](https://github.com/google/ExoPlayer/issues/7344),
+ [#8339](https://github.com/google/ExoPlayer/issues/8339)).
+ * Fix a bug that could cause the next content position played after a seek
+ to snap back to the cue point of the preceding ad, rather than the
+ requested content position.
+ * Fix a regression that caused an ad group to be skipped after an initial
+ seek to a non-zero position. Unsupported VPAID ads will still be
+ skipped, but only after the preload timeout rather than instantly
+ ([#8428](https://github.com/google/ExoPlayer/issues/8428),
+ [#7832](https://github.com/google/ExoPlayer/issues/7832)).
+ * Fix a regression that caused a short ad followed by another ad to be
+ skipped due to playback being stuck buffering waiting for the second ad
+ to load ([#8492](https://github.com/google/ExoPlayer/issues/8492)).
+* FFmpeg extension:
+ * Link the FFmpeg library statically, saving 350KB in binary size on
+ average.
+* OkHttp extension:
+ * Add `OkHttpDataSource.Factory` and deprecate `OkHttpDataSourceFactory`.
+* Cronet extension:
+ * Add `CronetDataSource.Factory` and deprecate `CronetDataSourceFactory`.
+ * Support setting the user agent on `CronetDataSource.Factory` and
+ `CronetEngineWrapper`.
+* MediaSession extension:
+ * Support `setPlaybackSpeed(float)` and disable it by default. Use
+ `MediaSessionConnector.setEnabledPlaybackActions(long)` to enable
+ ([#8229](https://github.com/google/ExoPlayer/issues/8229)).
+* Remove deprecated symbols:
+ * `AdaptiveMediaSourceEventListener`. Use `MediaSourceEventListener`
+ instead.
+ * `DashMediaSource.Factory.setMinLoadableRetryCount(int)`. Use
+ `DashMediaSource.Factory.setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)`
+ instead.
+ * `DefaultAnalyticsListener`. Use `AnalyticsListener` instead.
+ * `DefaultLoadControl` constructors. Use `DefaultLoadControl.Builder`
+ instead.
+ * `DrmInitData.get(UUID)`. Use `DrmInitData.get(int)` and
+ `DrmInitData.SchemeData.matches(UUID)` instead.
+ * `ExtractorsMediaSource.Factory.setMinLoadableRetryCount(int)`. Use
+ `ExtractorsMediaSource.Factory.setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)`
+ instead.
+ * `FixedTrackSelection.Factory`. If you need to disable adaptive
+ selection in `DefaultTrackSelector`, enable the
+ `DefaultTrackSelector.Parameters.forceHighestSupportedBitrate` flag.
+ * `HlsMediaSource.Factory.setMinLoadableRetryCount(int)`. Use
+ `HlsMediaSource.Factory.setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)`
+ instead.
+ * `MappedTrackInfo.getTrackFormatSupport(int, int, int)`. Use
+ `MappedTrackInfo.getTrackSupport(int, int, int)` instead.
+ * `MappedTrackInfo.getTrackTypeRendererSupport(int)`. Use
+ `MappedTrackInfo.getTypeSupport(int)` instead.
+ * `MappedTrackInfo.getUnassociatedTrackGroups()`. Use
+ `MappedTrackInfo.getUnmappedTrackGroups()` instead.
+ * `MappedTrackInfo.length`. Use `MappedTrackInfo.getRendererCount()`
+ instead.
+ * `Player.DefaultEventListener.onTimelineChanged(Timeline, Object)`.
+ Use `Player.EventListener.onTimelineChanged(Timeline, int)` instead.
+ * `Player.setAudioAttributes(AudioAttributes)`. Use
+ `Player.AudioComponent.setAudioAttributes(AudioAttributes, boolean)`
+ instead.
+ * `PlayerView.setDefaultArtwork(Bitmap)`. Use
+ `PlayerView.setDefaultArtwork(Drawable)` instead.
+ * `PlayerView.setShowBuffering(boolean)`. Use
+ `PlayerView.setShowBuffering(int)` instead.
+ * `SimpleExoPlayer.clearMetadataOutput(MetadataOutput)`. Use
+ `SimpleExoPlayer.removeMetadataOutput(MetadataOutput)` instead.
+ * `SimpleExoPlayer.clearTextOutput(TextOutput)`. Use
+ `SimpleExoPlayer.removeTextOutput(TextOutput)` instead.
+ * `SimpleExoPlayer.clearVideoListener()`. Use
+ `SimpleExoPlayer.removeVideoListener(VideoListener)` instead.
+ * `SimpleExoPlayer.getAudioStreamType()`. Use
+ `SimpleExoPlayer.getAudioAttributes()` instead.
+ * `SimpleExoPlayer.setAudioDebugListener(AudioRendererEventListener)`.
+ Use `SimpleExoPlayer.addAnalyticsListener(AnalyticsListener)` instead.
+ * `SimpleExoPlayer.setAudioStreamType(int)`. Use
+ `SimpleExoPlayer.setAudioAttributes(AudioAttributes)` instead.
+ * `SimpleExoPlayer.setMetadataOutput(MetadataOutput)`. Use
+ `SimpleExoPlayer.addMetadataOutput(MetadataOutput)` instead. If your
+ application is calling `SimpleExoPlayer.setMetadataOutput(null)`, make
+ sure to replace this call with a call to
+ `SimpleExoPlayer.removeMetadataOutput(MetadataOutput)`.
+ * `SimpleExoPlayer.setPlaybackParams(PlaybackParams)`. Use
+ `SimpleExoPlayer.setPlaybackParameters(PlaybackParameters)` instead.
+ * `SimpleExoPlayer.setTextOutput(TextOutput)`. Use
+ `SimpleExoPlayer.addTextOutput(TextOutput)` instead. If your
+ application is calling `SimpleExoPlayer.setTextOutput(null)`, make sure
+ to replace this call with a call to
+ `SimpleExoPlayer.removeTextOutput(TextOutput)`.
+ * `SimpleExoPlayer.setVideoDebugListener(VideoRendererEventListener)`.
+ Use `SimpleExoPlayer.addAnalyticsListener(AnalyticsListener)` instead.
+ * `SimpleExoPlayer.setVideoListener(VideoListener)`. Use
+ `SimpleExoPlayer.addVideoListener(VideoListener)` instead. If your
+ application is calling `SimpleExoPlayer.setVideoListener(null)`, make
+ sure to replace this call with a call to
+ `SimpleExoPlayer.removeVideoListener(VideoListener)`.
+ * `SimpleExoPlayer.VideoListener`. Use
+ `com.google.android.exoplayer2.video.VideoListener` instead.
+ * `SingleSampleMediaSource.EventListener` and constructors. Use
+ `MediaSourceEventListener` and `SingleSampleMediaSource.Factory`
+ instead.
+ * `SimpleExoPlayer.addVideoDebugListener`,
+ `SimpleExoPlayer.removeVideoDebugListener`,
+ `SimpleExoPlayer.addAudioDebugListener` and
+ `SimpleExoPlayer.removeAudioDebugListener`. Use
+ `SimpleExoPlayer.addAnalyticsListener` and
+ `SimpleExoPlayer.removeAnalyticsListener` instead.
+ * `SingleSampleMediaSource.Factory.setMinLoadableRetryCount(int)`. Use
+ `SingleSampleMediaSource.Factory.setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)`
+ instead.
+ * `SsMediaSource.Factory.setMinLoadableRetryCount(int)`. Use
+ `SsMediaSource.Factory.setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy)`
+ instead.
+
### 2.12.3 (2021-01-13) ###
* Core library:
diff --git a/build.gradle b/build.gradle
index 8c044f2fdaa..8c2186522ae 100644
--- a/build.gradle
+++ b/build.gradle
@@ -26,7 +26,6 @@ allprojects {
repositories {
google()
jcenter()
- maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}
project.ext {
exoplayerPublishEnabled = false
diff --git a/constants.gradle b/constants.gradle
index 50ce661b9a7..bb775e70509 100644
--- a/constants.gradle
+++ b/constants.gradle
@@ -13,18 +13,18 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
- releaseVersion = '2.12.3'
- releaseVersionCode = 2012003
+ releaseVersion = '2.13.0'
+ releaseVersionCode = 2013000
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
- compileSdkVersion = 29
+ compileSdkVersion = 30
dexmakerVersion = '2.21.0'
junitVersion = '4.13-rc-2'
guavaVersion = '27.1-android'
mockitoVersion = '2.28.2'
mockWebServerVersion = '3.12.0'
- robolectricVersion = '4.4'
+ robolectricVersion = '4.5'
checkerframeworkVersion = '3.3.0'
checkerframeworkCompatVersion = '2.5.0'
jsr305Version = '3.0.2'
@@ -33,14 +33,15 @@ project.ext {
androidxAppCompatVersion = '1.1.0'
androidxCollectionVersion = '1.1.0'
androidxFuturesVersion = '1.1.0'
- androidxMediaVersion = '1.0.1'
+ androidxMediaVersion = '1.2.1'
androidxMedia2Version = '1.1.0'
androidxMultidexVersion = '2.0.0'
androidxRecyclerViewVersion = '1.1.0'
- androidxTestCoreVersion = '1.2.0'
+ androidxTestCoreVersion = '1.3.0'
androidxTestJUnitVersion = '1.1.1'
- androidxTestRunnerVersion = '1.2.0'
- androidxTestRulesVersion = '1.2.0'
+ androidxTestRunnerVersion = '1.3.0'
+ androidxTestRulesVersion = '1.3.0'
+ androidxTestServicesStorageVersion = '1.3.0'
truthVersion = '1.0'
modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
diff --git a/core_settings.gradle b/core_settings.gradle
index bd217a37e56..241b94a19ba 100644
--- a/core_settings.gradle
+++ b/core_settings.gradle
@@ -28,6 +28,7 @@ include modulePrefix + 'library-dash'
include modulePrefix + 'library-extractor'
include modulePrefix + 'library-hls'
include modulePrefix + 'library-smoothstreaming'
+include modulePrefix + 'library-transformer'
include modulePrefix + 'library-ui'
include modulePrefix + 'robolectricutils'
include modulePrefix + 'testutils'
@@ -56,6 +57,7 @@ project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/d
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
+project(modulePrefix + 'library-transformer').projectDir = new File(rootDir, 'library/transformer')
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
project(modulePrefix + 'robolectricutils').projectDir = new File(rootDir, 'robolectricutils')
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java
index 9dc82e0b236..8c367b8734d 100644
--- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java
+++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java
@@ -312,7 +312,8 @@ private void setCurrentPlayer(Player currentPlayer) {
windowIndex = currentItemIndex;
}
}
- previousPlayer.stop(true);
+ previousPlayer.stop();
+ previousPlayer.clearMediaItems();
}
this.currentPlayer = currentPlayer;
diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java
index dc0a8b990ac..191602dfb8c 100644
--- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java
+++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java
@@ -152,7 +152,7 @@ private void initializePlayer() {
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback);
} else {
- drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
+ drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED;
}
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json
index 4d9e1df23a9..b515eca98a2 100644
--- a/demos/main/src/main/assets/media.exolist.json
+++ b/demos/main/src/main/assets/media.exolist.json
@@ -354,6 +354,23 @@
"name": "VMAP midroll ad pod at 5 s with 10 skippable ads",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
"ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-10-skippable-ads"
+ },
+ {
+ "name": "Playlist with three ad tags",
+ "playlist": [
+ {
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
+ },
+ {
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-25s.mp4",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator="
+ },
+ {
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
+ "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
+ }
+ ]
}
]
},
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java
index 2d15dfcbb48..080387db7e1 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java
@@ -16,11 +16,13 @@
package com.google.android.exoplayer2.demo;
import android.content.Context;
+import android.os.Build;
import com.google.android.exoplayer2.DefaultRenderersFactory;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
-import com.google.android.exoplayer2.ext.cronet.CronetDataSourceFactory;
+import com.google.android.exoplayer2.ext.cronet.CronetDataSource;
import com.google.android.exoplayer2.ext.cronet.CronetEngineWrapper;
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
@@ -28,6 +30,7 @@
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
@@ -36,6 +39,9 @@
import com.google.android.exoplayer2.util.Log;
import java.io.File;
import java.io.IOException;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
import java.util.concurrent.Executors;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@@ -44,6 +50,22 @@ public final class DemoUtil {
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
+ /**
+ * Whether the demo application uses Cronet for networking. Note that Cronet does not provide
+ * automatic support for cookies (https://github.com/google/ExoPlayer/issues/5975).
+ *
+ *
If set to false, the platform's default network stack is used with a {@link CookieManager}
+ * configured in {@link #getHttpDataSourceFactory}.
+ */
+ private static final boolean USE_CRONET_FOR_NETWORKING = true;
+
+ private static final String USER_AGENT =
+ "ExoPlayerDemo/"
+ + ExoPlayerLibraryInfo.VERSION
+ + " (Linux; Android "
+ + Build.VERSION.RELEASE
+ + ") "
+ + ExoPlayerLibraryInfo.VERSION_SLASHY;
private static final String TAG = "DemoUtil";
private static final String DOWNLOAD_ACTION_FILE = "actions";
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
@@ -78,10 +100,18 @@ public static RenderersFactory buildRenderersFactory(
public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) {
if (httpDataSourceFactory == null) {
- context = context.getApplicationContext();
- CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper(context);
- httpDataSourceFactory =
- new CronetDataSourceFactory(cronetEngineWrapper, Executors.newSingleThreadExecutor());
+ if (USE_CRONET_FOR_NETWORKING) {
+ context = context.getApplicationContext();
+ CronetEngineWrapper cronetEngineWrapper =
+ new CronetEngineWrapper(context, USER_AGENT, /* preferGMSCoreCronet= */ false);
+ httpDataSourceFactory =
+ new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor());
+ } else {
+ CookieManager cookieManager = new CookieManager();
+ cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
+ CookieHandler.setDefault(cookieManager);
+ httpDataSourceFactory = new DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT);
+ }
}
return httpDataSourceFactory;
}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
index 80c64bc9bd9..2cf2671abab 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
@@ -332,7 +332,7 @@ private void onDownloadPrepared(DownloadHelper helper) {
/* titleId= */ R.string.exo_download_description,
mappedTrackInfo,
trackSelectorParameters,
- /* allowAdaptiveSelections =*/ false,
+ /* allowAdaptiveSelections= */ false,
/* allowMultipleOverrides= */ true,
/* onClickListener= */ this,
/* onDismissListener= */ this);
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java
index d2d962c568e..d1cb0357187 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java
@@ -173,7 +173,9 @@ private static void addPlaybackPropertiesToIntent(
.putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, playbackProperties.mimeType)
.putExtra(
AD_TAG_URI_EXTRA + extrasKeySuffix,
- playbackProperties.adTagUri != null ? playbackProperties.adTagUri.toString() : null);
+ playbackProperties.adsConfiguration != null
+ ? playbackProperties.adsConfiguration.adTagUri.toString()
+ : null);
if (playbackProperties.drmConfiguration != null) {
addDrmConfigurationToIntent(playbackProperties.drmConfiguration, intent, extrasKeySuffix);
}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
index e1a698acdca..5fb342be432 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
@@ -19,7 +19,6 @@
import android.content.Intent;
import android.content.pm.PackageManager;
-import android.net.Uri;
import android.os.Bundle;
import android.util.Pair;
import android.view.KeyEvent;
@@ -59,9 +58,6 @@
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.EventLogger;
import com.google.android.exoplayer2.util.Util;
-import java.net.CookieHandler;
-import java.net.CookieManager;
-import java.net.CookiePolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -78,13 +74,6 @@ public class PlayerActivity extends AppCompatActivity
private static final String KEY_POSITION = "position";
private static final String KEY_AUTO_PLAY = "auto_play";
- private static final CookieManager DEFAULT_COOKIE_MANAGER;
-
- static {
- DEFAULT_COOKIE_MANAGER = new CookieManager();
- DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
- }
-
protected StyledPlayerView playerView;
protected LinearLayout debugRootView;
protected TextView debugTextView;
@@ -102,20 +91,16 @@ public class PlayerActivity extends AppCompatActivity
private int startWindow;
private long startPosition;
- // Fields used only for ad playback.
+ // For ad playback only.
private AdsLoader adsLoader;
- private Uri loadedAdTagUri;
- // Activity lifecycle
+ // Activity lifecycle.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dataSourceFactory = DemoUtil.getDataSourceFactory(/* context= */ this);
- if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
- CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
- }
setContentView();
debugRootView = findViewById(R.id.controls_root);
@@ -348,7 +333,7 @@ private List createMediaItems(Intent intent) {
return Collections.emptyList();
}
}
- hasAds |= mediaItem.playbackProperties.adTagUri != null;
+ hasAds |= mediaItem.playbackProperties.adsConfiguration != null;
}
if (!hasAds) {
releaseAdsLoader();
@@ -356,16 +341,7 @@ private List createMediaItems(Intent intent) {
return mediaItems;
}
- private AdsLoader getAdsLoader(Uri adTagUri) {
- if (mediaItems.size() > 1) {
- showToast(R.string.unsupported_ads_in_playlist);
- releaseAdsLoader();
- return null;
- }
- if (!adTagUri.equals(loadedAdTagUri)) {
- releaseAdsLoader();
- loadedAdTagUri = adTagUri;
- }
+ private AdsLoader getAdsLoader(MediaItem.AdsConfiguration adsConfiguration) {
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
if (adsLoader == null) {
adsLoader = new ImaAdsLoader.Builder(/* context= */ this).build();
@@ -394,7 +370,6 @@ private void releaseAdsLoader() {
if (adsLoader != null) {
adsLoader.release();
adsLoader = null;
- loadedAdTagUri = null;
playerView.getOverlayFrameLayout().removeAllViews();
}
}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
index ea5b38ce8e8..a66a1e03014 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
@@ -252,7 +252,7 @@ private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) {
}
MediaItem.PlaybackProperties playbackProperties =
checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties);
- if (playbackProperties.adTagUri != null) {
+ if (playbackProperties.adsConfiguration != null) {
return R.string.download_ads_unsupported;
}
String scheme = playbackProperties.uri.getScheme();
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
index d3f9b3880da..5b50298df0d 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
@@ -94,7 +94,7 @@ public static TrackSelectionDialog createForTrackSelector(
/* titleId= */ R.string.track_selection_title,
mappedTrackInfo,
/* initialParameters = */ parameters,
- /* allowAdaptiveSelections =*/ true,
+ /* allowAdaptiveSelections= */ true,
/* allowMultipleOverrides= */ false,
/* onClickListener= */ (dialog, which) -> {
DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon();
diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml
index 9fe882b1629..49441ef7dac 100644
--- a/demos/main/src/main/res/values/strings.xml
+++ b/demos/main/src/main/res/values/strings.xml
@@ -45,8 +45,6 @@
One or more sample lists failed to load
- Playing without ads, as ads are not supported in playlists
-
Failed to start downloadFailed to obtain offline license
diff --git a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java
index eb669ecf946..a31cd7efe01 100644
--- a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java
+++ b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java
@@ -197,7 +197,7 @@ private void initializePlayer() {
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback);
} else {
- drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
+ drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED;
}
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
diff --git a/extensions/av1/proguard-rules.txt b/extensions/av1/proguard-rules.txt
index 9d73f7e2b58..c4ef2286f22 100644
--- a/extensions/av1/proguard-rules.txt
+++ b/extensions/av1/proguard-rules.txt
@@ -5,3 +5,7 @@
native ;
}
+# Some members of this class are being accessed from native methods. Keep them unobfuscated.
+-keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer {
+ *;
+}
diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java
index ad8c8a682cc..08d48e9699e 100644
--- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java
+++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java
@@ -15,10 +15,12 @@
*/
package com.google.android.exoplayer2.ext.av1;
+import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
import static java.lang.Runtime.getRuntime;
import android.view.Surface;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
@@ -28,7 +30,8 @@
import java.nio.ByteBuffer;
/** Gav1 decoder. */
-/* package */ final class Gav1Decoder
+@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
+public final class Gav1Decoder
extends SimpleDecoder {
// LINT.IfChange
diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java
index 7c558d24b2d..a083cd8414d 100644
--- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java
+++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java
@@ -15,12 +15,15 @@
*/
package com.google.android.exoplayer2.ext.av1;
+import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION;
+
import android.os.Handler;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
@@ -42,7 +45,10 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer {
private static final String TAG = "Libgav1VideoRenderer";
private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4;
private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4;
- /* Default size based on 720p resolution video compressed by a factor of two. */
+ /**
+ * Default input buffer size in bytes, based on 720p resolution video compressed by a factor of
+ * two.
+ */
private static final int DEFAULT_INPUT_BUFFER_SIZE =
Util.ceilDivide(1280, 64) * Util.ceilDivide(720, 64) * (64 * 64 * 3 / 2) / 2;
@@ -124,12 +130,13 @@ public String getName() {
public final int supportsFormat(Format format) {
if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType)
|| !Gav1Library.isAvailable()) {
- return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
+ return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
}
if (format.exoMediaCryptoType != null) {
- return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
+ return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_DRM);
}
- return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
+ return RendererCapabilities.create(
+ C.FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
}
@Override
@@ -164,7 +171,13 @@ protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
}
@Override
- protected boolean canKeepCodec(Format oldFormat, Format newFormat) {
- return true;
+ protected DecoderReuseEvaluation canReuseDecoder(
+ String decoderName, Format oldFormat, Format newFormat) {
+ return new DecoderReuseEvaluation(
+ decoderName,
+ oldFormat,
+ newFormat,
+ REUSE_RESULT_YES_WITHOUT_RECONFIGURATION,
+ /* discardReasons= */ 0);
}
}
diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle
index 4c8f648e344..d0cc501fcb4 100644
--- a/extensions/cast/build.gradle
+++ b/extensions/cast/build.gradle
@@ -16,7 +16,7 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
api 'com.google.android.gms:play-services-cast-framework:18.1.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
- implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-common')
implementation project(modulePrefix + 'library-ui')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
index eeda98d2d9e..d20b84cbc3e 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
@@ -27,13 +27,14 @@
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
-import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.util.ListenerSet;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.gms.cast.CastStatusCodes;
@@ -49,11 +50,8 @@
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Iterator;
+import java.util.Collections;
import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
@@ -97,9 +95,7 @@ public final class CastPlayer extends BasePlayer {
private final SeekResultCallback seekResultCallback;
// Listeners and notification.
- private final CopyOnWriteArrayList listeners;
- private final ArrayList notificationsBatch;
- private final ArrayDeque ongoingNotificationsTasks;
+ private final ListenerSet listeners;
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state.
@@ -138,9 +134,12 @@ public CastPlayer(CastContext castContext, MediaItemConverter mediaItemConverter
period = new Timeline.Period();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
- listeners = new CopyOnWriteArrayList<>();
- notificationsBatch = new ArrayList<>();
- ongoingNotificationsTasks = new ArrayDeque<>();
+ listeners =
+ new ListenerSet<>(
+ Looper.getMainLooper(),
+ Clock.DEFAULT,
+ Player.Events::new,
+ (listener, eventFlags) -> listener.onEvents(/* player= */ this, eventFlags));
playWhenReady = new StateHolder<>(false);
repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
@@ -293,18 +292,12 @@ public Looper getApplicationLooper() {
@Override
public void addListener(EventListener listener) {
- Assertions.checkNotNull(listener);
- listeners.addIfAbsent(new ListenerHolder(listener));
+ listeners.add(listener);
}
@Override
public void removeListener(EventListener listener) {
- for (ListenerHolder listenerHolder : listeners) {
- if (listenerHolder.listener.equals(listener)) {
- listenerHolder.release();
- listeners.remove(listenerHolder);
- }
- }
+ listeners.remove(listener);
}
@Override
@@ -416,7 +409,7 @@ public void setPlayWhenReady(boolean playWhenReady) {
// the local state will be updated to reflect the state reported by the Cast SDK.
setPlayerStateAndNotifyIfChanged(
playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, playbackState);
- flushNotifications();
+ listeners.flushEvents();
PendingResult pendingResult =
playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause();
this.playWhenReady.pendingResultCallback =
@@ -425,7 +418,7 @@ public void setPlayWhenReady(boolean playWhenReady) {
public void onResult(MediaChannelResult mediaChannelResult) {
if (remoteMediaClient != null) {
updatePlayerStateAndNotifyIfChanged(this);
- flushNotifications();
+ listeners.flushEvents();
}
}
};
@@ -456,13 +449,13 @@ public void seekTo(int windowIndex, long positionMs) {
pendingSeekCount++;
pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs;
- notificationsBatch.add(
- new ListenerNotificationTask(
- listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
+ listeners.queueEvent(
+ Player.EVENT_POSITION_DISCONTINUITY,
+ listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
} else if (pendingSeekCount == 0) {
- notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
+ listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed);
}
- flushNotifications();
+ listeners.flushEvents();
}
@Override
@@ -511,12 +504,6 @@ public int getRendererType(int index) {
}
}
- @Override
- @Nullable
- public TrackSelector getTrackSelector() {
- return null;
- }
-
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient == null) {
@@ -526,7 +513,7 @@ public void setRepeatMode(@RepeatMode int repeatMode) {
// operation to be perceived as synchronous by the user. When the operation reports a result,
// the local state will be updated to reflect the state reported by the Cast SDK.
setRepeatModeAndNotifyIfChanged(repeatMode);
- flushNotifications();
+ listeners.flushEvents();
PendingResult pendingResult =
remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), /* jsonObject= */ null);
this.repeatMode.pendingResultCallback =
@@ -535,7 +522,7 @@ public void setRepeatMode(@RepeatMode int repeatMode) {
public void onResult(MediaChannelResult mediaChannelResult) {
if (remoteMediaClient != null) {
updateRepeatModeAndNotifyIfChanged(this);
- flushNotifications();
+ listeners.flushEvents();
}
}
};
@@ -568,6 +555,12 @@ public TrackGroupArray getCurrentTrackGroups() {
return currentTrackGroups;
}
+ @Override
+ public List getCurrentStaticMetadata() {
+ // CastPlayer does not currently support metadata.
+ return Collections.emptyList();
+ }
+
@Override
public Timeline getCurrentTimeline() {
return currentTimeline;
@@ -654,8 +647,8 @@ private void updateInternalStateAndNotifyIfChanged() {
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
if (wasPlaying != isPlaying) {
- notificationsBatch.add(
- new ListenerNotificationTask(listener -> listener.onIsPlayingChanged(isPlaying)));
+ listeners.queueEvent(
+ Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying));
}
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
updateTimelineAndNotifyIfChanged();
@@ -671,17 +664,16 @@ private void updateInternalStateAndNotifyIfChanged() {
}
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
this.currentWindowIndex = currentWindowIndex;
- notificationsBatch.add(
- new ListenerNotificationTask(
- listener ->
- listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
+ listeners.queueEvent(
+ Player.EVENT_POSITION_DISCONTINUITY,
+ listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION));
}
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
- notificationsBatch.add(
- new ListenerNotificationTask(
- listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
+ listeners.queueEvent(
+ Player.EVENT_TRACKS_CHANGED,
+ listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection));
}
- flushNotifications();
+ listeners.flushEvents();
}
/**
@@ -720,11 +712,11 @@ private void updateTimelineAndNotifyIfChanged() {
if (updateTimeline()) {
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
- notificationsBatch.add(
- new ListenerNotificationTask(
- listener ->
- listener.onTimelineChanged(
- currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
+ listeners.queueEvent(
+ Player.EVENT_TIMELINE_CHANGED,
+ listener ->
+ listener.onTimelineChanged(
+ currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
}
}
@@ -773,7 +765,7 @@ private boolean updateTracksAndSelectionsAndNotifyIfChanged() {
int rendererIndex = getRendererIndexForTrackType(trackType);
if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET
&& trackSelections[rendererIndex] == null) {
- trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0);
+ trackSelections[rendererIndex] = new CastTrackSelection(trackGroups[i]);
}
}
TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups);
@@ -843,8 +835,8 @@ private PendingResult removeMediaItemsInternal(int[] uids) {
private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
if (this.repeatMode.value != repeatMode) {
this.repeatMode.value = repeatMode;
- notificationsBatch.add(
- new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode)));
+ listeners.queueEvent(
+ Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
}
}
@@ -858,17 +850,19 @@ private void setPlayerStateAndNotifyIfChanged(
if (playWhenReadyChanged || playbackStateChanged) {
this.playbackState = playbackState;
this.playWhenReady.value = playWhenReady;
- notificationsBatch.add(
- new ListenerNotificationTask(
- listener -> {
- listener.onPlayerStateChanged(playWhenReady, playbackState);
- if (playbackStateChanged) {
- listener.onPlaybackStateChanged(playbackState);
- }
- if (playWhenReadyChanged) {
- listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason);
- }
- }));
+ listeners.queueEvent(
+ /* eventFlag= */ C.INDEX_UNSET,
+ listener -> listener.onPlayerStateChanged(playWhenReady, playbackState));
+ if (playbackStateChanged) {
+ listeners.queueEvent(
+ Player.EVENT_PLAYBACK_STATE_CHANGED,
+ listener -> listener.onPlaybackStateChanged(playbackState));
+ }
+ if (playWhenReadyChanged) {
+ listeners.queueEvent(
+ Player.EVENT_PLAY_WHEN_READY_CHANGED,
+ listener -> listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason));
+ }
}
}
@@ -976,20 +970,6 @@ private static int getCastRepeatMode(@RepeatMode int repeatMode) {
}
}
- private void flushNotifications() {
- boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
- ongoingNotificationsTasks.addAll(notificationsBatch);
- notificationsBatch.clear();
- if (recursiveNotification) {
- // This will be handled once the current notification task is finished.
- return;
- }
- while (!ongoingNotificationsTasks.isEmpty()) {
- ongoingNotificationsTasks.peekFirst().execute();
- ongoingNotificationsTasks.removeFirst();
- }
- }
-
private MediaQueueItem[] toMediaQueueItems(List mediaItems) {
MediaQueueItem[] mediaQueueItems = new MediaQueueItem[mediaItems.size()];
for (int i = 0; i < mediaItems.size(); i++) {
@@ -1100,8 +1080,7 @@ public void onResult(MediaChannelResult result) {
if (--pendingSeekCount == 0) {
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
- notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
- flushNotifications();
+ listeners.sendEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed);
}
}
}
@@ -1141,21 +1120,4 @@ public boolean acceptsUpdate(@Nullable ResultCallback> resultCallback) {
return pendingResultCallback == resultCallback;
}
}
-
- private final class ListenerNotificationTask {
-
- private final Iterator listenersSnapshot;
- private final ListenerInvocation listenerInvocation;
-
- private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
- this.listenersSnapshot = listeners.iterator();
- this.listenerInvocation = listenerInvocation;
- }
-
- public void execute() {
- while (listenersSnapshot.hasNext()) {
- listenersSnapshot.next().invoke(listenerInvocation);
- }
- }
- }
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
index edd2a060d28..1a03b779f60 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
@@ -46,8 +46,8 @@ public static final class ItemData {
private ItemData() {
this(
- /* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */
- C.TIME_UNSET,
+ /* durationUs= */ C.TIME_UNSET,
+ /* defaultPositionUs= */ C.TIME_UNSET,
/* isLive= */ false);
}
@@ -126,16 +126,18 @@ public int getWindowCount() {
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET;
+ MediaItem mediaItem =
+ new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build();
return window.set(
/* uid= */ ids[windowIndex],
- /* mediaItem= */ new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build(),
+ /* mediaItem= */ mediaItem,
/* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic,
isDynamic,
- isLive[windowIndex],
+ isLive[windowIndex] ? mediaItem.liveConfiguration : null,
defaultPositionsUs[windowIndex],
durationUs,
/* firstPeriodIndex= */ windowIndex,
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTrackSelection.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTrackSelection.java
new file mode 100644
index 00000000000..22fe86d9e4e
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTrackSelection.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 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.cast;
+
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * {@link TrackSelection} that only selects the first track of the provided {@link TrackGroup}.
+ *
+ *
This relies on {@link CastPlayer} track groups only having one track.
+ */
+/* package */ class CastTrackSelection implements TrackSelection {
+
+ private final TrackGroup trackGroup;
+
+ /** @param trackGroup The {@link TrackGroup} from which the first track will only be selected. */
+ public CastTrackSelection(TrackGroup trackGroup) {
+ this.trackGroup = trackGroup;
+ }
+
+ @Override
+ public TrackGroup getTrackGroup() {
+ return trackGroup;
+ }
+
+ @Override
+ public int length() {
+ return 1;
+ }
+
+ @Override
+ public Format getFormat(int index) {
+ Assertions.checkArgument(index == 0);
+ return trackGroup.getFormat(0);
+ }
+
+ @Override
+ public int getIndexInTrackGroup(int index) {
+ return index == 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ @SuppressWarnings("ReferenceEquality")
+ public int indexOf(Format format) {
+ return format == trackGroup.getFormat(0) ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int indexOf(int indexInTrackGroup) {
+ return indexInTrackGroup == 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ // Object overrides.
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(trackGroup);
+ }
+
+ // Track groups are compared by identity not value, as distinct groups may have the same value.
+ @Override
+ @SuppressWarnings({"ReferenceEquality", "EqualsGetClass"})
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ CastTrackSelection other = (CastTrackSelection) obj;
+ return trackGroup == other.trackGroup;
+ }
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java
index 705f2c25085..c72a1fb316f 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java
@@ -45,7 +45,9 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
@Override
public MediaItem toMediaItem(MediaQueueItem item) {
// `item` came from `toMediaQueueItem()` so the custom JSON data must be set.
- return getMediaItem(Assertions.checkNotNull(item.getMedia().getCustomData()));
+ MediaInfo mediaInfo = item.getMedia();
+ Assertions.checkNotNull(mediaInfo);
+ return getMediaItem(Assertions.checkNotNull(mediaInfo.getCustomData()));
}
@Override
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTrackSelectionTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTrackSelectionTest.java
new file mode 100644
index 00000000000..0a30c0c4b82
--- /dev/null
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTrackSelectionTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2018 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.cast;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test for {@link CastTrackSelection}. */
+@RunWith(AndroidJUnit4.class)
+public class CastTrackSelectionTest {
+
+ private static final TrackGroup TRACK_GROUP =
+ new TrackGroup(new Format.Builder().build(), new Format.Builder().build());
+
+ private static final CastTrackSelection SELECTION = new CastTrackSelection(TRACK_GROUP);
+
+ @Test
+ public void length_isOne() {
+ assertThat(SELECTION.length()).isEqualTo(1);
+ }
+
+ @Test
+ public void getTrackGroup_returnsSameGroup() {
+ assertThat(SELECTION.getTrackGroup()).isSameInstanceAs(TRACK_GROUP);
+ }
+
+ @Test
+ public void getFormatSelectedTrack_isFirstTrack() {
+ assertThat(SELECTION.getFormat(0)).isSameInstanceAs(TRACK_GROUP.getFormat(0));
+ }
+
+ @Test
+ public void getIndexInTrackGroup_ofSelectedTrack_returnsFirstTrack() {
+ assertThat(SELECTION.getIndexInTrackGroup(0)).isEqualTo(0);
+ }
+
+ @Test
+ public void getIndexInTrackGroup_onePastTheEnd_returnsIndexUnset() {
+ assertThat(SELECTION.getIndexInTrackGroup(1)).isEqualTo(C.INDEX_UNSET);
+ }
+
+ @Test
+ public void indexOf_selectedTrack_returnsFirstTrack() {
+ assertThat(SELECTION.indexOf(0)).isEqualTo(0);
+ }
+
+ @Test
+ public void indexOf_onePastTheEnd_returnsIndexUnset() {
+ assertThat(SELECTION.indexOf(1)).isEqualTo(C.INDEX_UNSET);
+ }
+
+ @Test(expected = Exception.class)
+ public void getFormat_outOfBound_throws() {
+ CastTrackSelection selection = new CastTrackSelection(TRACK_GROUP);
+
+ selection.getFormat(1);
+ }
+}
diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle
index c0f443d5df7..f50304fb94a 100644
--- a/extensions/cronet/build.gradle
+++ b/extensions/cronet/build.gradle
@@ -13,14 +13,28 @@
// limitations under the License.
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
+android {
+ defaultConfig {
+ multiDexEnabled true
+ }
+}
+
dependencies {
api "com.google.android.gms:play-services-cronet:17.0.0"
- implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-common')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
+ androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
+ androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
+ androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
+ // Instrumentation tests assume that an app-packaged version of cronet is
+ // available.
+ androidTestImplementation 'org.chromium.net:cronet-embedded:72.3626.96'
+ androidTestImplementation(project(modulePrefix + 'testutils'))
testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
diff --git a/extensions/cronet/src/androidTest/AndroidManifest.xml b/extensions/cronet/src/androidTest/AndroidManifest.xml
new file mode 100644
index 00000000000..96e8e54f571
--- /dev/null
+++ b/extensions/cronet/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceContractTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceContractTest.java
new file mode 100644
index 00000000000..967c894c397
--- /dev/null
+++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceContractTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.cronet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.testutil.DataSourceContractTest;
+import com.google.android.exoplayer2.testutil.HttpDataSourceTestEnv;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.common.collect.ImmutableList;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+/** {@link DataSource} contract tests for {@link CronetDataSource}. */
+@RunWith(AndroidJUnit4.class)
+public class CronetDataSourceContractTest extends DataSourceContractTest {
+
+ @Rule public HttpDataSourceTestEnv httpDataSourceTestEnv = new HttpDataSourceTestEnv();
+ private final ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+ @After
+ public void tearDown() {
+ executorService.shutdown();
+ }
+
+ @Override
+ protected DataSource createDataSource() {
+ CronetEngineWrapper cronetEngineWrapper =
+ new CronetEngineWrapper(
+ ApplicationProvider.getApplicationContext(),
+ /* userAgent= */ "test-agent",
+ /* preferGMSCoreCronet= */ false);
+ assertThat(cronetEngineWrapper.getCronetEngineSource())
+ .isEqualTo(CronetEngineWrapper.SOURCE_NATIVE);
+ return new CronetDataSource.Factory(cronetEngineWrapper, executorService)
+ .setFallbackFactory(new InvalidDataSourceFactory())
+ .createDataSource();
+ }
+
+ @Override
+ protected ImmutableList getTestResources() {
+ return httpDataSourceTestEnv.getServedResources();
+ }
+
+ @Override
+ protected Uri getNotFoundUri() {
+ return Uri.parse(httpDataSourceTestEnv.getNonexistentUrl());
+ }
+
+ /**
+ * An {@link HttpDataSource.Factory} that throws {@link UnsupportedOperationException} on every
+ * interaction.
+ */
+ private static class InvalidDataSourceFactory implements HttpDataSource.Factory {
+ @Override
+ public HttpDataSource createDataSource() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public HttpDataSource.RequestProperties getDefaultRequestProperties() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public HttpDataSource.Factory setDefaultRequestProperties(
+ Map defaultRequestProperties) {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
index b215b6d763d..2726b00c734 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
@@ -17,7 +17,6 @@
import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.max;
-import static java.lang.Math.min;
import android.net.Uri;
import android.text.TextUtils;
@@ -25,15 +24,19 @@
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.upstream.BaseDataSource;
+import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Predicate;
+import com.google.common.primitives.Ints;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketTimeoutException;
@@ -65,6 +68,204 @@
*/
public class CronetDataSource extends BaseDataSource implements HttpDataSource {
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.cronet");
+ }
+
+ /** {@link DataSource.Factory} for {@link CronetDataSource} instances. */
+ public static final class Factory implements HttpDataSource.Factory {
+
+ private final CronetEngineWrapper cronetEngineWrapper;
+ private final Executor executor;
+ private final RequestProperties defaultRequestProperties;
+ private final DefaultHttpDataSource.Factory internalFallbackFactory;
+
+ @Nullable private HttpDataSource.Factory fallbackFactory;
+ @Nullable private Predicate contentTypePredicate;
+ @Nullable private TransferListener transferListener;
+ @Nullable private String userAgent;
+ private int connectTimeoutMs;
+ private int readTimeoutMs;
+ private boolean resetTimeoutOnRedirects;
+ private boolean handleSetCookieRequests;
+
+ /**
+ * Creates an instance.
+ *
+ * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This
+ * may be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a
+ * thread hop from Cronet's internal network thread to the response handling thread.
+ * However, to avoid slowing down overall network performance, care must be taken to make
+ * sure response handling is a fast operation when using a direct executor.
+ */
+ public Factory(CronetEngineWrapper cronetEngineWrapper, Executor executor) {
+ this.cronetEngineWrapper = cronetEngineWrapper;
+ this.executor = executor;
+ defaultRequestProperties = new RequestProperties();
+ internalFallbackFactory = new DefaultHttpDataSource.Factory();
+ connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS;
+ readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
+ }
+
+ /** @deprecated Use {@link #setDefaultRequestProperties(Map)} instead. */
+ @Deprecated
+ @Override
+ public final RequestProperties getDefaultRequestProperties() {
+ return defaultRequestProperties;
+ }
+
+ @Override
+ public final Factory setDefaultRequestProperties(Map defaultRequestProperties) {
+ this.defaultRequestProperties.clearAndSet(defaultRequestProperties);
+ internalFallbackFactory.setDefaultRequestProperties(defaultRequestProperties);
+ return this;
+ }
+
+ /**
+ * Sets the user agent that will be used.
+ *
+ *
The default is {@code null}, which causes the default user agent of the underlying {@link
+ * CronetEngine} to be used.
+ *
+ * @param userAgent The user agent that will be used, or {@code null} to use the default user
+ * agent of the underlying {@link CronetEngine}.
+ * @return This factory.
+ */
+ public Factory setUserAgent(@Nullable String userAgent) {
+ this.userAgent = userAgent;
+ internalFallbackFactory.setUserAgent(userAgent);
+ return this;
+ }
+
+ /**
+ * Sets the connect timeout, in milliseconds.
+ *
+ *
The default is {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS}.
+ *
+ * @param connectTimeoutMs The connect timeout, in milliseconds, that will be used.
+ * @return This factory.
+ */
+ public Factory setConnectionTimeoutMs(int connectTimeoutMs) {
+ this.connectTimeoutMs = connectTimeoutMs;
+ internalFallbackFactory.setConnectTimeoutMs(connectTimeoutMs);
+ return this;
+ }
+
+ /**
+ * Sets whether the connect timeout is reset when a redirect occurs.
+ *
+ *
The default is {@code false}.
+ *
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
+ * @return This factory.
+ */
+ public Factory setResetTimeoutOnRedirects(boolean resetTimeoutOnRedirects) {
+ this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
+ return this;
+ }
+
+ /**
+ * Sets whether "Set-Cookie" requests on redirect should be forwarded to the redirect url in the
+ * "Cookie" header.
+ *
+ *
The default is {@code false}.
+ *
+ * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded
+ * to the redirect url in the "Cookie" header.
+ * @return This factory.
+ */
+ public Factory setHandleSetCookieRequests(boolean handleSetCookieRequests) {
+ this.handleSetCookieRequests = handleSetCookieRequests;
+ return this;
+ }
+
+ /**
+ * Sets the read timeout, in milliseconds.
+ *
+ *
The default is {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS}.
+ *
+ * @param readTimeoutMs The connect timeout, in milliseconds, that will be used.
+ * @return This factory.
+ */
+ public Factory setReadTimeoutMs(int readTimeoutMs) {
+ this.readTimeoutMs = readTimeoutMs;
+ internalFallbackFactory.setReadTimeoutMs(readTimeoutMs);
+ return this;
+ }
+
+ /**
+ * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
+ * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
+ *
+ *
The default is {@code null}.
+ *
+ * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
+ * predicate that was previously set.
+ * @return This factory.
+ */
+ public Factory setContentTypePredicate(@Nullable Predicate contentTypePredicate) {
+ this.contentTypePredicate = contentTypePredicate;
+ internalFallbackFactory.setContentTypePredicate(contentTypePredicate);
+ return this;
+ }
+
+ /**
+ * Sets the {@link TransferListener} that will be used.
+ *
+ *
The default is {@code null}.
+ *
+ *
See {@link DataSource#addTransferListener(TransferListener)}.
+ *
+ * @param transferListener The listener that will be used.
+ * @return This factory.
+ */
+ public Factory setTransferListener(@Nullable TransferListener transferListener) {
+ this.transferListener = transferListener;
+ internalFallbackFactory.setTransferListener(transferListener);
+ return this;
+ }
+
+ /**
+ * Sets the fallback {@link HttpDataSource.Factory} that is used as a fallback if the {@link
+ * CronetEngineWrapper} fails to provide a {@link CronetEngine}.
+ *
+ *
By default a {@link DefaultHttpDataSource} is used as fallback factory.
+ *
+ * @param fallbackFactory The fallback factory that will be used.
+ * @return This factory.
+ */
+ public Factory setFallbackFactory(@Nullable HttpDataSource.Factory fallbackFactory) {
+ this.fallbackFactory = fallbackFactory;
+ return this;
+ }
+
+ @Override
+ public HttpDataSource createDataSource() {
+ @Nullable CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine();
+ if (cronetEngine == null) {
+ return (fallbackFactory != null)
+ ? fallbackFactory.createDataSource()
+ : internalFallbackFactory.createDataSource();
+ }
+ CronetDataSource dataSource =
+ new CronetDataSource(
+ cronetEngine,
+ executor,
+ connectTimeoutMs,
+ readTimeoutMs,
+ resetTimeoutOnRedirects,
+ handleSetCookieRequests,
+ userAgent,
+ defaultRequestProperties,
+ contentTypePredicate);
+ if (transferListener != null) {
+ dataSource.addTransferListener(transferListener);
+ }
+ return dataSource;
+ }
+ }
+
/**
* Thrown when an error is encountered when trying to open a {@link CronetDataSource}.
*/
@@ -85,20 +286,11 @@ public OpenException(String errorMessage, DataSpec dataSpec, int cronetConnectio
super(errorMessage, dataSpec, TYPE_OPEN);
this.cronetConnectionStatus = cronetConnectionStatus;
}
-
}
- static {
- ExoPlayerLibraryInfo.registerModule("goog.exo.cronet");
- }
-
- /**
- * The default connection timeout, in milliseconds.
- */
+ /** The default connection timeout, in milliseconds. */
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
- /**
- * The default read timeout, in milliseconds.
- */
+ /** The default read timeout, in milliseconds. */
public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
/* package */ final UrlRequest.Callback urlRequestCallback;
@@ -119,6 +311,7 @@ public OpenException(String errorMessage, DataSpec dataSpec, int cronetConnectio
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
private final boolean handleSetCookieRequests;
+ @Nullable private final String userAgent;
@Nullable private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private final ConditionVariable operation;
@@ -149,16 +342,9 @@ public OpenException(String errorMessage, DataSpec dataSpec, int cronetConnectio
private volatile long currentConnectTimeoutMs;
- /**
- * Creates an instance.
- *
- * @param cronetEngine A CronetEngine.
- * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
- * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
- * hop from Cronet's internal network thread to the response handling thread. However, to
- * avoid slowing down overall network performance, care must be taken to make sure response
- * handling is a fast operation when using a direct executor.
- */
+ /** @deprecated Use {@link CronetDataSource.Factory} instead. */
+ @SuppressWarnings("deprecation")
+ @Deprecated
public CronetDataSource(CronetEngine cronetEngine, Executor executor) {
this(
cronetEngine,
@@ -169,21 +355,8 @@ public CronetDataSource(CronetEngine cronetEngine, Executor executor) {
/* defaultRequestProperties= */ null);
}
- /**
- * Creates an instance.
- *
- * @param cronetEngine A CronetEngine.
- * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
- * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
- * hop from Cronet's internal network thread to the response handling thread. However, to
- * avoid slowing down overall network performance, care must be taken to make sure response
- * handling is a fast operation when using a direct executor.
- * @param connectTimeoutMs The connection timeout, in milliseconds.
- * @param readTimeoutMs The read timeout, in milliseconds.
- * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
- * server as HTTP headers on every request.
- */
+ /** @deprecated Use {@link CronetDataSource.Factory} instead. */
+ @Deprecated
public CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
@@ -197,28 +370,14 @@ public CronetDataSource(
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
- Clock.DEFAULT,
+ /* handleSetCookieRequests= */ false,
+ /* userAgent= */ null,
defaultRequestProperties,
- /* handleSetCookieRequests= */ false);
+ /* contentTypePredicate= */ null);
}
- /**
- * Creates an instance.
- *
- * @param cronetEngine A CronetEngine.
- * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
- * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
- * hop from Cronet's internal network thread to the response handling thread. However, to
- * avoid slowing down overall network performance, care must be taken to make sure response
- * handling is a fast operation when using a direct executor.
- * @param connectTimeoutMs The connection timeout, in milliseconds.
- * @param readTimeoutMs The read timeout, in milliseconds.
- * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
- * server as HTTP headers on every request.
- * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
- * the redirect url in the "Cookie" header.
- */
+ /** @deprecated Use {@link CronetDataSource.Factory} instead. */
+ @Deprecated
public CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
@@ -233,26 +392,13 @@ public CronetDataSource(
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
- Clock.DEFAULT,
+ handleSetCookieRequests,
+ /* userAgent= */ null,
defaultRequestProperties,
- handleSetCookieRequests);
+ /* contentTypePredicate= */ null);
}
- /**
- * Creates an instance.
- *
- * @param cronetEngine A CronetEngine.
- * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
- * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
- * hop from Cronet's internal network thread to the response handling thread. However, to
- * avoid slowing down overall network performance, care must be taken to make sure response
- * handling is a fast operation when using a direct executor.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * #open(DataSpec)}.
- * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link
- * #setContentTypePredicate(Predicate)}.
- */
+ /** @deprecated Use {@link CronetDataSource.Factory} instead. */
@SuppressWarnings("deprecation")
@Deprecated
public CronetDataSource(
@@ -269,26 +415,7 @@ public CronetDataSource(
/* defaultRequestProperties= */ null);
}
- /**
- * Creates an instance.
- *
- * @param cronetEngine A CronetEngine.
- * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
- * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
- * hop from Cronet's internal network thread to the response handling thread. However, to
- * avoid slowing down overall network performance, care must be taken to make sure response
- * handling is a fast operation when using a direct executor.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * #open(DataSpec)}.
- * @param connectTimeoutMs The connection timeout, in milliseconds.
- * @param readTimeoutMs The read timeout, in milliseconds.
- * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
- * server as HTTP headers on every request.
- * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
- * RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
- */
+ /** @deprecated Use {@link CronetDataSource.Factory} instead. */
@SuppressWarnings("deprecation")
@Deprecated
public CronetDataSource(
@@ -310,28 +437,7 @@ public CronetDataSource(
/* handleSetCookieRequests= */ false);
}
- /**
- * Creates an instance.
- *
- * @param cronetEngine A CronetEngine.
- * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
- * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
- * hop from Cronet's internal network thread to the response handling thread. However, to
- * avoid slowing down overall network performance, care must be taken to make sure response
- * handling is a fast operation when using a direct executor.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * #open(DataSpec)}.
- * @param connectTimeoutMs The connection timeout, in milliseconds.
- * @param readTimeoutMs The read timeout, in milliseconds.
- * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
- * server as HTTP headers on every request.
- * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
- * the redirect url in the "Cookie" header.
- * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
- * RequestProperties, boolean)} and {@link #setContentTypePredicate(Predicate)}.
- */
+ /** @deprecated Use {@link CronetDataSource.Factory} instead. */
@Deprecated
public CronetDataSource(
CronetEngine cronetEngine,
@@ -348,31 +454,34 @@ public CronetDataSource(
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
- Clock.DEFAULT,
+ handleSetCookieRequests,
+ /* userAgent= */ null,
defaultRequestProperties,
- handleSetCookieRequests);
- this.contentTypePredicate = contentTypePredicate;
+ contentTypePredicate);
}
- /* package */ CronetDataSource(
+ private CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
- Clock clock,
+ boolean handleSetCookieRequests,
+ @Nullable String userAgent,
@Nullable RequestProperties defaultRequestProperties,
- boolean handleSetCookieRequests) {
+ @Nullable Predicate contentTypePredicate) {
super(/* isNetwork= */ true);
- this.urlRequestCallback = new UrlRequestCallback();
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
this.executor = Assertions.checkNotNull(executor);
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
- this.clock = Assertions.checkNotNull(clock);
- this.defaultRequestProperties = defaultRequestProperties;
this.handleSetCookieRequests = handleSetCookieRequests;
+ this.userAgent = userAgent;
+ this.defaultRequestProperties = defaultRequestProperties;
+ this.contentTypePredicate = contentTypePredicate;
+ clock = Clock.DEFAULT;
+ urlRequestCallback = new UrlRequestCallback();
requestProperties = new RequestProperties();
operation = new ConditionVariable();
}
@@ -384,6 +493,7 @@ public CronetDataSource(
* @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
* predicate that was previously set.
*/
+ @Deprecated
public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) {
this.contentTypePredicate = contentTypePredicate;
}
@@ -464,19 +574,11 @@ public long open(DataSpec dataSpec) throws HttpDataSourceException {
UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
int responseCode = responseInfo.getHttpStatusCode();
if (responseCode < 200 || responseCode > 299) {
- byte[] responseBody = Util.EMPTY_BYTE_ARRAY;
- ByteBuffer readBuffer = getOrCreateReadBuffer();
- while (!readBuffer.hasRemaining()) {
- operation.close();
- readBuffer.clear();
- readInternal(readBuffer);
- if (finished) {
- break;
- }
- readBuffer.flip();
- int existingResponseBodyEnd = responseBody.length;
- responseBody = Arrays.copyOf(responseBody, responseBody.length + readBuffer.remaining());
- readBuffer.get(responseBody, existingResponseBodyEnd, readBuffer.remaining());
+ byte[] responseBody;
+ try {
+ responseBody = readResponseBody();
+ } catch (HttpDataSourceException e) {
+ responseBody = Util.EMPTY_BYTE_ARRAY;
}
InvalidResponseCodeException exception =
@@ -553,14 +655,21 @@ public int read(byte[] buffer, int offset, int readLength) throws HttpDataSource
readBuffer.flip();
Assertions.checkState(readBuffer.hasRemaining());
if (bytesToSkip > 0) {
- int bytesSkipped = (int) min(readBuffer.remaining(), bytesToSkip);
+ int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
readBuffer.position(readBuffer.position() + bytesSkipped);
bytesToSkip -= bytesSkipped;
}
}
}
- int bytesRead = min(readBuffer.remaining(), readLength);
+ // Ensure we read up to bytesRemaining, in case this was a Range request with finite end, but
+ // the server does not support Range requests and transmitted the entire resource.
+ int bytesRead =
+ Ints.min(
+ bytesRemaining != C.LENGTH_UNSET ? (int) bytesRemaining : Integer.MAX_VALUE,
+ readBuffer.remaining(),
+ readLength);
+
readBuffer.get(buffer, offset, bytesRead);
if (bytesRemaining != C.LENGTH_UNSET) {
@@ -707,9 +816,7 @@ protected UrlResponseInfo getCurrentUrlResponseInfo() {
return responseInfo;
}
- // Internal methods.
-
- private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
+ protected UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
UrlRequest.Builder requestBuilder =
cronetEngine
.newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor)
@@ -744,6 +851,9 @@ private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOExcep
}
requestBuilder.addHeader("Range", rangeValue.toString());
}
+ if (userAgent != null) {
+ requestBuilder.addHeader("User-Agent", userAgent);
+ }
// TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed
// (adjusting the code as necessary).
// Force identity encoding unless gzip is allowed.
@@ -759,6 +869,8 @@ private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOExcep
return requestBuilder;
}
+ // Internal methods.
+
private boolean blockUntilConnectTimeout() throws InterruptedException {
long now = clock.elapsedRealtime();
boolean opened = false;
@@ -773,6 +885,29 @@ private void resetConnectTimeout() {
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
}
+ /**
+ * Reads the whole response body.
+ *
+ * @return The response body.
+ * @throws HttpDataSourceException If an error occurs reading from the source.
+ */
+ private byte[] readResponseBody() throws HttpDataSourceException {
+ byte[] responseBody = Util.EMPTY_BYTE_ARRAY;
+ ByteBuffer readBuffer = getOrCreateReadBuffer();
+ while (!finished) {
+ operation.close();
+ readBuffer.clear();
+ readInternal(readBuffer);
+ readBuffer.flip();
+ if (readBuffer.remaining() > 0) {
+ int existingResponseBodyEnd = responseBody.length;
+ responseBody = Arrays.copyOf(responseBody, responseBody.length + readBuffer.remaining());
+ readBuffer.get(responseBody, existingResponseBodyEnd, readBuffer.remaining());
+ }
+ }
+ return responseBody;
+ }
+
/**
* Reads up to {@code buffer.remaining()} bytes of data from {@code currentUrlRequest} and stores
* them into {@code buffer}. If there is an error and {@code buffer == readBuffer}, then it resets
@@ -911,7 +1046,7 @@ private static boolean isEmpty(@Nullable List> list) {
// Copy as much as possible from the src buffer into dst buffer.
// Returns the number of bytes copied.
private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) {
- int remaining = min(src.remaining(), dst.remaining());
+ int remaining = Math.min(src.remaining(), dst.remaining());
int limit = src.limit();
src.limit(src.position() + remaining);
dst.put(src);
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
index 85c9d09a791..df3e9549e5a 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
@@ -15,20 +15,17 @@
*/
package com.google.android.exoplayer2.ext.cronet;
-import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT;
import androidx.annotation.Nullable;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
-import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener;
import java.util.concurrent.Executor;
import org.chromium.net.CronetEngine;
-/**
- * A {@link Factory} that produces {@link CronetDataSource}.
- */
+/** @deprecated Use {@link CronetDataSource.Factory} instead. */
+@Deprecated
public final class CronetDataSourceFactory extends BaseFactory {
/**
@@ -83,7 +80,7 @@ public CronetDataSourceFactory(
* Creates an instance.
*
*
If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
*
Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
@@ -92,24 +89,26 @@ public CronetDataSourceFactory(
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
*/
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor) {
- this(cronetEngineWrapper, executor, DEFAULT_USER_AGENT);
+ this(cronetEngineWrapper, executor, /* userAgent= */ (String) null);
}
/**
* Creates an instance.
*
*
If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
*
Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param userAgent A user agent used to create a fallback HttpDataSource if needed.
+ * @param userAgent The user agent that will be used by the fallback {@link HttpDataSource} if
+ * needed, or {@code null} for the fallback to use the default user agent of the underlying
+ * platform.
*/
public CronetDataSourceFactory(
- CronetEngineWrapper cronetEngineWrapper, Executor executor, String userAgent) {
+ CronetEngineWrapper cronetEngineWrapper, Executor executor, @Nullable String userAgent) {
this(
cronetEngineWrapper,
executor,
@@ -117,26 +116,23 @@ public CronetDataSourceFactory(
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
false,
- new DefaultHttpDataSourceFactory(
- userAgent,
- /* listener= */ null,
- DEFAULT_CONNECT_TIMEOUT_MILLIS,
- DEFAULT_READ_TIMEOUT_MILLIS,
- false));
+ new DefaultHttpDataSource.Factory().setUserAgent(userAgent));
}
/**
* Creates an instance.
*
*
If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param userAgent A user agent used to create a fallback HttpDataSource if needed.
+ * @param userAgent The user agent that will be used by the fallback {@link HttpDataSource} if
+ * needed, or {@code null} for the fallback to use the default user agent of the underlying
+ * platform.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
@@ -144,20 +140,18 @@ public CronetDataSourceFactory(
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
- String userAgent) {
+ @Nullable String userAgent) {
this(
cronetEngineWrapper,
executor,
/* transferListener= */ null,
- DEFAULT_CONNECT_TIMEOUT_MILLIS,
- DEFAULT_READ_TIMEOUT_MILLIS,
+ connectTimeoutMs,
+ readTimeoutMs,
resetTimeoutOnRedirects,
- new DefaultHttpDataSourceFactory(
- userAgent,
- /* listener= */ null,
- connectTimeoutMs,
- readTimeoutMs,
- resetTimeoutOnRedirects));
+ new DefaultHttpDataSource.Factory()
+ .setUserAgent(userAgent)
+ .setConnectTimeoutMs(connectTimeoutMs)
+ .setReadTimeoutMs(readTimeoutMs));
}
/**
@@ -225,7 +219,7 @@ public CronetDataSourceFactory(
* Creates an instance.
*
*
If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
*
Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
@@ -238,14 +232,14 @@ public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
@Nullable TransferListener transferListener) {
- this(cronetEngineWrapper, executor, transferListener, DEFAULT_USER_AGENT);
+ this(cronetEngineWrapper, executor, transferListener, /* userAgent= */ (String) null);
}
/**
* Creates an instance.
*
*
If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
*
Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
@@ -253,13 +247,15 @@ public CronetDataSourceFactory(
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
* @param transferListener An optional listener.
- * @param userAgent A user agent used to create a fallback HttpDataSource if needed.
+ * @param userAgent The user agent that will be used by the fallback {@link HttpDataSource} if
+ * needed, or {@code null} for the fallback to use the default user agent of the underlying
+ * platform.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
@Nullable TransferListener transferListener,
- String userAgent) {
+ @Nullable String userAgent) {
this(
cronetEngineWrapper,
executor,
@@ -267,19 +263,16 @@ public CronetDataSourceFactory(
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
false,
- new DefaultHttpDataSourceFactory(
- userAgent,
- transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS,
- DEFAULT_READ_TIMEOUT_MILLIS,
- false));
+ new DefaultHttpDataSource.Factory()
+ .setUserAgent(userAgent)
+ .setTransferListener(transferListener));
}
/**
* Creates an instance.
*
*
If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
@@ -287,7 +280,9 @@ public CronetDataSourceFactory(
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param userAgent A user agent used to create a fallback HttpDataSource if needed.
+ * @param userAgent The user agent that will be used by the fallback {@link HttpDataSource} if
+ * needed, or {@code null} for the fallback to use the default user agent of the underlying
+ * platform.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
@@ -296,16 +291,19 @@ public CronetDataSourceFactory(
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
- String userAgent) {
+ @Nullable String userAgent) {
this(
cronetEngineWrapper,
executor,
transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS,
- DEFAULT_READ_TIMEOUT_MILLIS,
+ connectTimeoutMs,
+ readTimeoutMs,
resetTimeoutOnRedirects,
- new DefaultHttpDataSourceFactory(
- userAgent, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects));
+ new DefaultHttpDataSource.Factory()
+ .setUserAgent(userAgent)
+ .setTransferListener(transferListener)
+ .setConnectTimeoutMs(connectTimeoutMs)
+ .setReadTimeoutMs(readTimeoutMs));
}
/**
@@ -343,7 +341,7 @@ public CronetDataSourceFactory(
@Override
protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties
defaultRequestProperties) {
- CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine();
+ @Nullable CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine();
if (cronetEngine == null) {
return fallbackFactory.createDataSource();
}
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
index 9f709b14d03..d9332342e35 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
@@ -73,25 +73,29 @@ public final class CronetEngineWrapper {
public static final int SOURCE_UNAVAILABLE = 4;
/**
- * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
- * {@link CronetProvider}. Sets wrapper to prefer natively bundled Cronet over GMSCore Cronet
- * if both are available.
+ * Creates a wrapper for a {@link CronetEngine} built using the most suitable {@link
+ * CronetProvider}. When natively bundled Cronet and GMSCore Cronet are both available, the
+ * natively bundled provider is preferred.
*
* @param context A context.
*/
public CronetEngineWrapper(Context context) {
- this(context, false);
+ this(context, /* userAgent= */ null, /* preferGMSCoreCronet= */ false);
}
/**
- * Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
- * {@link CronetProvider} based on user preference.
+ * Creates a wrapper for a {@link CronetEngine} built using the most suitable {@link
+ * CronetProvider}. When natively bundled Cronet and GMSCore Cronet are both available, {@code
+ * preferGMSCoreCronet} determines which is preferred.
*
* @param context A context.
+ * @param userAgent A default user agent, or {@code null} to use a default user agent of the
+ * {@link CronetEngine}.
* @param preferGMSCoreCronet Whether Cronet from GMSCore should be preferred over natively
* bundled Cronet if both are available.
*/
- public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
+ public CronetEngineWrapper(
+ Context context, @Nullable String userAgent, boolean preferGMSCoreCronet) {
CronetEngine cronetEngine = null;
@CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
List cronetProviders = new ArrayList<>(CronetProvider.getAllProviders(context));
@@ -108,7 +112,11 @@ public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
for (int i = 0; i < cronetProviders.size() && cronetEngine == null; i++) {
String providerName = cronetProviders.get(i).getName();
try {
- cronetEngine = cronetProviders.get(i).createBuilder().build();
+ CronetEngine.Builder cronetEngineBuilder = cronetProviders.get(i).createBuilder();
+ if (userAgent != null) {
+ cronetEngineBuilder.setUserAgent(userAgent);
+ }
+ cronetEngine = cronetEngineBuilder.build();
if (providerComparator.isNativeProvider(providerName)) {
cronetEngineSource = SOURCE_NATIVE;
} else if (providerComparator.isGMSCoreProvider(providerName)) {
@@ -133,9 +141,9 @@ public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
}
/**
- * Creates a wrapper for an existing CronetEngine.
+ * Creates a wrapper for an existing {@link CronetEngine}.
*
- * @param cronetEngine An existing CronetEngine.
+ * @param cronetEngine The CronetEngine to wrap.
*/
public CronetEngineWrapper(CronetEngine cronetEngine) {
this.cronetEngine = cronetEngine;
diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
index b0b80a8e128..631e1300d60 100644
--- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
+++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static java.lang.Math.min;
+import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -32,13 +33,15 @@
import android.net.Uri;
import android.os.ConditionVariable;
import android.os.SystemClock;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
+import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer2.upstream.TransferListener;
-import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.io.InterruptedIOException;
@@ -53,8 +56,12 @@
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
+import okhttp3.Headers;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
import org.chromium.net.CronetEngine;
import org.chromium.net.NetworkException;
import org.chromium.net.UrlRequest;
@@ -100,21 +107,18 @@ public final class CronetDataSourceTest {
public void setUp() {
MockitoAnnotations.initMocks(this);
- HttpDataSource.RequestProperties defaultRequestProperties =
- new HttpDataSource.RequestProperties();
- defaultRequestProperties.set("defaultHeader1", "defaultValue1");
- defaultRequestProperties.set("defaultHeader2", "defaultValue2");
+ Map defaultRequestProperties = new HashMap<>();
+ defaultRequestProperties.put("defaultHeader1", "defaultValue1");
+ defaultRequestProperties.put("defaultHeader2", "defaultValue2");
dataSourceUnderTest =
- new CronetDataSource(
- mockCronetEngine,
- mockExecutor,
- TEST_CONNECT_TIMEOUT_MS,
- TEST_READ_TIMEOUT_MS,
- /* resetTimeoutOnRedirects= */ true,
- Clock.DEFAULT,
- defaultRequestProperties,
- /* handleSetCookieRequests= */ false);
+ (CronetDataSource)
+ new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor)
+ .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
+ .setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
+ .setResetTimeoutOnRedirects(true)
+ .setDefaultRequestProperties(defaultRequestProperties)
+ .createDataSource();
dataSourceUnderTest.addTransferListener(mockTransferListener);
when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
@@ -376,7 +380,8 @@ public void requestOpenFailDueToDnsFailure() {
}
@Test
- public void requestOpenPropagatesFailureResponseBody() throws Exception {
+ public void requestOpen_withNon2xxResponseCode_throwsInvalidResponseCodeExceptionWithBody()
+ throws Exception {
mockResponseStartSuccess();
// Use a size larger than CronetDataSource.READ_BUFFER_SIZE_BYTES
int responseLength = 40 * 1024;
@@ -385,8 +390,8 @@ public void requestOpenPropagatesFailureResponseBody() throws Exception {
try {
dataSourceUnderTest.open(testDataSpec);
- fail("HttpDataSource.InvalidResponseCodeException expected");
- } catch (HttpDataSource.InvalidResponseCodeException e) {
+ fail("InvalidResponseCodeException expected");
+ } catch (InvalidResponseCodeException e) {
assertThat(e.responseBody).isEqualTo(buildTestDataArray(0, responseLength));
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
@@ -395,6 +400,26 @@ public void requestOpenPropagatesFailureResponseBody() throws Exception {
}
}
+ @Test
+ public void
+ requestOpen_withNon2xxResponseCode_andRequestBodyReadFailure_throwsInvalidResponseCodeExceptionWithoutBody()
+ throws Exception {
+ mockResponseStartSuccess();
+ mockReadFailure();
+ testUrlResponseInfo = createUrlResponseInfo(/* statusCode= */ 500);
+
+ try {
+ dataSourceUnderTest.open(testDataSpec);
+ fail("InvalidResponseCodeException expected");
+ } catch (InvalidResponseCodeException e) {
+ assertThat(e.responseBody).isEmpty();
+ // Check for connection not automatically closed.
+ verify(mockUrlRequest, never()).cancel();
+ verify(mockTransferListener, never())
+ .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
+ }
+ }
+
@Test
public void requestOpenValidatesContentTypePredicate() {
mockResponseStartSuccess();
@@ -1134,15 +1159,13 @@ public void redirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_follow
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
throws HttpDataSourceException {
dataSourceUnderTest =
- new CronetDataSource(
- mockCronetEngine,
- mockExecutor,
- TEST_CONNECT_TIMEOUT_MS,
- TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
- Clock.DEFAULT,
- null,
- true);
+ (CronetDataSource)
+ new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor)
+ .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
+ .setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
+ .setResetTimeoutOnRedirects(true)
+ .setHandleSetCookieRequests(true)
+ .createDataSource();
dataSourceUnderTest.addTransferListener(mockTransferListener);
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
@@ -1164,15 +1187,13 @@ public void redirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_follow
throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
dataSourceUnderTest =
- new CronetDataSource(
- mockCronetEngine,
- mockExecutor,
- TEST_CONNECT_TIMEOUT_MS,
- TEST_READ_TIMEOUT_MS,
- /* resetTimeoutOnRedirects= */ true,
- Clock.DEFAULT,
- /* defaultRequestProperties= */ null,
- /* handleSetCookieRequests= */ true);
+ (CronetDataSource)
+ new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor)
+ .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
+ .setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
+ .setResetTimeoutOnRedirects(true)
+ .setHandleSetCookieRequests(true)
+ .createDataSource();
dataSourceUnderTest.addTransferListener(mockTransferListener);
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
@@ -1202,15 +1223,13 @@ public void redirectNoSetCookieFollowsRedirect() throws HttpDataSourceException
public void redirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
throws HttpDataSourceException {
dataSourceUnderTest =
- new CronetDataSource(
- mockCronetEngine,
- mockExecutor,
- TEST_CONNECT_TIMEOUT_MS,
- TEST_READ_TIMEOUT_MS,
- /* resetTimeoutOnRedirects= */ true,
- Clock.DEFAULT,
- /* defaultRequestProperties= */ null,
- /* handleSetCookieRequests= */ true);
+ (CronetDataSource)
+ new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor)
+ .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
+ .setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
+ .setResetTimeoutOnRedirects(true)
+ .setHandleSetCookieRequests(true)
+ .createDataSource();
dataSourceUnderTest.addTransferListener(mockTransferListener);
mockSingleRedirectSuccess();
mockFollowRedirectSuccess();
@@ -1356,6 +1375,97 @@ public void allowDirectExecutor() throws HttpDataSourceException {
verify(mockUrlRequestBuilder).allowDirectExecutor();
}
+ @Test
+ public void factorySetFallbackHttpDataSourceFactory_cronetNotAvailable_usesFallbackFactory()
+ throws HttpDataSourceException, InterruptedException {
+ MockWebServer mockWebServer = new MockWebServer();
+ mockWebServer.enqueue(new MockResponse());
+ CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper((CronetEngine) null);
+ DefaultHttpDataSource.Factory fallbackFactory =
+ new DefaultHttpDataSource.Factory().setUserAgent("customFallbackFactoryUserAgent");
+ HttpDataSource dataSourceUnderTest =
+ new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor())
+ .setFallbackFactory(fallbackFactory)
+ .createDataSource();
+
+ dataSourceUnderTest.open(
+ new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build());
+
+ Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders();
+ assertThat(headers.get("user-agent")).isEqualTo("customFallbackFactoryUserAgent");
+ }
+
+ @Test
+ public void
+ factory_noFallbackFactoryCronetNotAvailable_delegateTransferListenerToInternalFallbackFactory()
+ throws HttpDataSourceException, InterruptedException {
+ MockWebServer mockWebServer = new MockWebServer();
+ mockWebServer.enqueue(new MockResponse());
+ CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper((CronetEngine) null);
+ HttpDataSource dataSourceUnderTest =
+ new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor())
+ .setTransferListener(mockTransferListener)
+ .createDataSource();
+ DataSpec dataSpec =
+ new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build();
+
+ dataSourceUnderTest.open(dataSpec);
+
+ verify(mockTransferListener)
+ .onTransferInitializing(eq(dataSourceUnderTest), eq(dataSpec), /* isNetwork= */ eq(true));
+ verify(mockTransferListener)
+ .onTransferStart(eq(dataSourceUnderTest), eq(dataSpec), /* isNetwork= */ eq(true));
+ }
+
+ @Test
+ public void
+ factory_noFallbackFactoryCronetNotAvailable_delegateDefaultRequestPropertiesToInternalFallbackFactory()
+ throws HttpDataSourceException, InterruptedException {
+ MockWebServer mockWebServer = new MockWebServer();
+ mockWebServer.enqueue(new MockResponse());
+ CronetEngineWrapper cronetEngineWrapper =
+ new CronetEngineWrapper(ApplicationProvider.getApplicationContext());
+ Map defaultRequestProperties = new HashMap<>();
+ defaultRequestProperties.put("0", "defaultRequestProperty0");
+ HttpDataSource dataSourceUnderTest =
+ new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor())
+ .setDefaultRequestProperties(defaultRequestProperties)
+ .createDataSource();
+
+ dataSourceUnderTest.open(
+ new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build());
+
+ Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders();
+ assertThat(headers.get("0")).isEqualTo("defaultRequestProperty0");
+ assertThat(dataSourceUnderTest).isInstanceOf(DefaultHttpDataSource.class);
+ }
+
+ @Test
+ public void
+ factory_noFallbackFactoryCronetNotAvailable_delegateDefaultRequestPropertiesToInternalFallbackFactoryAfterCreation()
+ throws HttpDataSourceException, InterruptedException {
+ MockWebServer mockWebServer = new MockWebServer();
+ mockWebServer.enqueue(new MockResponse());
+ CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper((CronetEngine) null);
+ Map defaultRequestProperties = new HashMap<>();
+ defaultRequestProperties.put("0", "defaultRequestProperty0");
+ CronetDataSource.Factory factory =
+ new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor());
+ HttpDataSource dataSourceUnderTest =
+ factory.setDefaultRequestProperties(defaultRequestProperties).createDataSource();
+ defaultRequestProperties.clear();
+ defaultRequestProperties.put("1", "defaultRequestPropertyAfterCreation");
+ factory.setDefaultRequestProperties(defaultRequestProperties);
+
+ dataSourceUnderTest.open(
+ new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build());
+
+ Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders();
+ assertThat(headers.get("0")).isNull();
+ assertThat(headers.get("1")).isEqualTo("defaultRequestPropertyAfterCreation");
+ assertThat(dataSourceUnderTest).isInstanceOf(DefaultHttpDataSource.class);
+ }
+
// Helper methods.
private void mockStatusResponse() {
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
index 0718dc2c5c7..878476d1a1a 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
@@ -91,19 +91,19 @@ public String getName() {
}
@Override
- @FormatSupport
+ @C.FormatSupport
protected int supportsFormatInternal(Format format) {
String mimeType = Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(mimeType)) {
- return FORMAT_UNSUPPORTED_TYPE;
+ return C.FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(mimeType)
|| (!sinkSupportsFormat(format, C.ENCODING_PCM_16BIT)
&& !sinkSupportsFormat(format, C.ENCODING_PCM_FLOAT))) {
- return FORMAT_UNSUPPORTED_SUBTYPE;
+ return C.FORMAT_UNSUPPORTED_SUBTYPE;
} else if (format.exoMediaCryptoType != null) {
- return FORMAT_UNSUPPORTED_DRM;
+ return C.FORMAT_UNSUPPORTED_DRM;
} else {
- return FORMAT_HANDLED;
+ return C.FORMAT_HANDLED;
}
}
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
index a9b862b2b28..6fb47e962b4 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
@@ -34,8 +34,7 @@ public final class FfmpegLibrary {
private static final String TAG = "FfmpegLibrary";
- private static final LibraryLoader LOADER =
- new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg_jni");
+ private static final LibraryLoader LOADER = new LibraryLoader("ffmpegJNI");
private static @MonotonicNonNull String version;
private static int inputBufferPaddingSize = C.LENGTH_UNSET;
@@ -145,10 +144,6 @@ public static boolean supportsFormat(String mimeType) {
return "pcm_mulaw";
case MimeTypes.AUDIO_ALAW:
return "pcm_alaw";
- case MimeTypes.VIDEO_H264:
- return "h264";
- case MimeTypes.VIDEO_H265:
- return "hevc";
default:
return null;
}
diff --git a/extensions/ffmpeg/src/main/jni/CMakeLists.txt b/extensions/ffmpeg/src/main/jni/CMakeLists.txt
index b60af4fa18a..fd3e398dd9a 100644
--- a/extensions/ffmpeg/src/main/jni/CMakeLists.txt
+++ b/extensions/ffmpeg/src/main/jni/CMakeLists.txt
@@ -3,17 +3,17 @@ cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
# Enable C++11 features.
set(CMAKE_CXX_STANDARD 11)
-project(libffmpeg_jni C CXX)
+project(libffmpegJNI C CXX)
set(ffmpeg_location "${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg")
set(ffmpeg_binaries "${ffmpeg_location}/android-libs/${ANDROID_ABI}")
foreach(ffmpeg_lib avutil swresample avcodec)
- set(ffmpeg_lib_filename lib${ffmpeg_lib}.so)
+ set(ffmpeg_lib_filename lib${ffmpeg_lib}.a)
set(ffmpeg_lib_file_path ${ffmpeg_binaries}/${ffmpeg_lib_filename})
add_library(
${ffmpeg_lib}
- SHARED
+ STATIC
IMPORTED)
set_target_properties(
${ffmpeg_lib} PROPERTIES
@@ -24,13 +24,13 @@ endforeach()
include_directories(${ffmpeg_location})
find_library(android_log_lib log)
-add_library(ffmpeg_jni
+add_library(ffmpegJNI
SHARED
ffmpeg_jni.cc)
-target_link_libraries(ffmpeg_jni
+target_link_libraries(ffmpegJNI
PRIVATE android
- PRIVATE avutil
PRIVATE swresample
PRIVATE avcodec
+ PRIVATE avutil
PRIVATE ${android_log_lib})
diff --git a/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh b/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh
index 4660669a336..7b2e9339021 100755
--- a/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh
+++ b/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh
@@ -19,10 +19,12 @@ FFMPEG_EXT_PATH=$1
NDK_PATH=$2
HOST_PLATFORM=$3
ENABLED_DECODERS=("${@:4}")
+JOBS=$(nproc 2> /dev/null || sysctl -n hw.ncpu 2> /dev/null || echo 4)
+echo "Using $JOBS jobs for make"
COMMON_OPTIONS="
--target-os=android
- --disable-static
- --enable-shared
+ --enable-static
+ --disable-shared
--disable-doc
--disable-programs
--disable-everything
@@ -48,11 +50,13 @@ cd "${FFMPEG_EXT_PATH}/jni/ffmpeg"
--cpu=armv7-a \
--cross-prefix="${TOOLCHAIN_PREFIX}/armv7a-linux-androideabi16-" \
--nm="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-nm" \
+ --ar="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-ar" \
+ --ranlib="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-ranlib" \
--strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \
--extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
--extra-ldflags="-Wl,--fix-cortex-a8" \
${COMMON_OPTIONS}
-make -j4
+make -j$JOBS
make install-libs
make clean
./configure \
@@ -61,9 +65,11 @@ make clean
--cpu=armv8-a \
--cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \
--nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \
+ --ar="${TOOLCHAIN_PREFIX}/aarch64-linux-android-ar" \
+ --ranlib="${TOOLCHAIN_PREFIX}/aarch64-linux-android-ranlib" \
--strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \
${COMMON_OPTIONS}
-make -j4
+make -j$JOBS
make install-libs
make clean
./configure \
@@ -72,10 +78,12 @@ make clean
--cpu=i686 \
--cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \
--nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \
+ --ar="${TOOLCHAIN_PREFIX}/i686-linux-android-ar" \
+ --ranlib="${TOOLCHAIN_PREFIX}/i686-linux-android-ranlib" \
--strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \
--disable-asm \
${COMMON_OPTIONS}
-make -j4
+make -j$JOBS
make install-libs
make clean
./configure \
@@ -84,9 +92,11 @@ make clean
--cpu=x86_64 \
--cross-prefix="${TOOLCHAIN_PREFIX}/x86_64-linux-android21-" \
--nm="${TOOLCHAIN_PREFIX}/x86_64-linux-android-nm" \
+ --ar="${TOOLCHAIN_PREFIX}/x86_64-linux-android-ar" \
+ --ranlib="${TOOLCHAIN_PREFIX}/x86_64-linux-android-ranlib" \
--strip="${TOOLCHAIN_PREFIX}/x86_64-linux-android-strip" \
--disable-asm \
${COMMON_OPTIONS}
-make -j4
+make -j$JOBS
make install-libs
make clean
diff --git a/extensions/flac/README.md b/extensions/flac/README.md
index a9d4c3094e2..47c74d11486 100644
--- a/extensions/flac/README.md
+++ b/extensions/flac/README.md
@@ -68,7 +68,7 @@ renderer.
### Using `FlacExtractor` ###
-`FlacExtractor` is used via `ExtractorMediaSource`. If you're using
+`FlacExtractor` is used via `ProgressiveMediaSource`. If you're using
`DefaultExtractorsFactory`, `FlacExtractor` will automatically be used to read
`.flac` files. If you're not using `DefaultExtractorsFactory`, return a
`FlacExtractor` from your `ExtractorsFactory.createExtractors` implementation.
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
index 84cb081b298..f6c5f6a6a73 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
@@ -15,7 +15,10 @@
*/
package com.google.android.exoplayer2.ext.flac;
+import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
+
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
@@ -27,11 +30,10 @@
import java.nio.ByteBuffer;
import java.util.List;
-/**
- * Flac decoder.
- */
-/* package */ final class FlacDecoder extends
- SimpleDecoder {
+/** Flac decoder. */
+@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
+public final class FlacDecoder
+ extends SimpleDecoder {
private final FlacStreamMetadata streamMetadata;
private final FlacDecoderJni decoderJni;
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
index df511866a38..4fea5ffa53a 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
@@ -79,11 +79,11 @@ public String getName() {
}
@Override
- @FormatSupport
+ @C.FormatSupport
protected int supportsFormatInternal(Format format) {
if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
- return FORMAT_UNSUPPORTED_TYPE;
+ return C.FORMAT_UNSUPPORTED_TYPE;
}
// Compute the format that the FLAC decoder will output.
Format outputFormat;
@@ -102,11 +102,11 @@ protected int supportsFormatInternal(Format format) {
outputFormat = getOutputFormat(streamMetadata);
}
if (!sinkSupportsFormat(outputFormat)) {
- return FORMAT_UNSUPPORTED_SUBTYPE;
+ return C.FORMAT_UNSUPPORTED_SUBTYPE;
} else if (format.exoMediaCryptoType != null) {
- return FORMAT_UNSUPPORTED_DRM;
+ return C.FORMAT_UNSUPPORTED_DRM;
} else {
- return FORMAT_HANDLED;
+ return C.FORMAT_HANDLED;
}
}
diff --git a/extensions/ima/README.md b/extensions/ima/README.md
index c67dfdbb5d5..016f848c7af 100644
--- a/extensions/ima/README.md
+++ b/extensions/ima/README.md
@@ -33,17 +33,19 @@ of the developer guide. The `AdsLoaderProvider` passed to the player's
extension only supports players which are accessed on the application's main
thread.
-Resuming the player after entering the background requires some special handling
-when playing ads. The player and its media source are released on entering the
-background, and are recreated when returning to the foreground. When playing ads
-it is necessary to persist ad playback state while in the background by keeping
-a reference to the `ImaAdsLoader`. When re-entering the foreground, pass the
-same instance back when `AdsLoaderProvider.getAdsLoader(Uri adTagUri)` is called
-to restore the state. It is also important to persist the player position when
-entering the background by storing the value of `player.getContentPosition()`.
-On returning to the foreground, seek to that position before preparing the new
-player instance. Finally, it is important to call `ImaAdsLoader.release()` when
-playback has finished and will not be resumed.
+Resuming the player after entering the background requires some special
+handling when playing ads. The player and its media source are released on
+entering the background, and are recreated when returning to the foreground.
+When playing ads it is necessary to persist ad playback state while in the
+background by keeping a reference to the `ImaAdsLoader`. When re-entering the
+foreground, pass the same instance back when
+`AdsLoaderProvider.getAdsLoader(MediaItem.AdsConfiguration adsConfiguration)`
+is called to restore the state. It is also important to persist the player
+position when entering the background by storing the value of
+`player.getContentPosition()`. On returning to the foreground, seek to that
+position before preparing the new player instance. Finally, it is important to
+call `ImaAdsLoader.release()` when playback has finished and will not be
+resumed.
You can try the IMA extension in the ExoPlayer demo app, which has test content
in the "IMA sample ad tags" section of the sample chooser. The demo app's
diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle
index 8cdfb0dffc7..72d40b8f8f7 100644
--- a/extensions/ima/build.gradle
+++ b/extensions/ima/build.gradle
@@ -25,7 +25,7 @@ android {
}
dependencies {
- api 'com.google.ads.interactivemedia.v3:interactivemedia:3.21.0'
+ api 'com.google.ads.interactivemedia.v3:interactivemedia:3.22.0'
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
diff --git a/extensions/ima/proguard-rules.txt b/extensions/ima/proguard-rules.txt
index feef3daf7a0..a46014de755 100644
--- a/extensions/ima/proguard-rules.txt
+++ b/extensions/ima/proguard-rules.txt
@@ -1,5 +1,6 @@
# Proguard rules specific to the IMA extension.
+-dontwarn com.google.ads.interactivemedia.**
-keep class com.google.ads.interactivemedia.** { *; }
-keep interface com.google.ads.interactivemedia.** { *; }
-keep class com.google.obf.** { *; }
diff --git a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java
index 9527d35cef9..839c8329516 100644
--- a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java
+++ b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java
@@ -248,6 +248,7 @@ protected MediaSource buildSource(
return new AdsMediaSource(
contentMediaSource,
adTagDataSpec,
+ /* adsId= */ adTagDataSpec.uri,
new DefaultMediaSourceFactory(dataSourceFactory),
Assertions.checkNotNull(imaAdsLoader),
new AdViewProvider() {
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java
new file mode 100644
index 00000000000..4ce0610fb67
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java
@@ -0,0 +1,1455 @@
+/*
+ * Copyright 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 static com.google.android.exoplayer2.ext.ima.ImaUtil.BITRATE_UNSET;
+import static com.google.android.exoplayer2.ext.ima.ImaUtil.TIMEOUT_UNSET;
+import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupTimesUsForCuePoints;
+import static com.google.android.exoplayer2.ext.ima.ImaUtil.getImaLooper;
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+import static com.google.android.exoplayer2.util.Assertions.checkState;
+import static java.lang.Math.max;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.view.ViewGroup;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
+import com.google.ads.interactivemedia.v3.api.AdError;
+import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
+import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
+import com.google.ads.interactivemedia.v3.api.AdEvent;
+import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener;
+import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType;
+import com.google.ads.interactivemedia.v3.api.AdPodInfo;
+import com.google.ads.interactivemedia.v3.api.AdsLoader;
+import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener;
+import com.google.ads.interactivemedia.v3.api.AdsManager;
+import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
+import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
+import com.google.ads.interactivemedia.v3.api.AdsRequest;
+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.ContentProgressProvider;
+import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
+import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.ads.AdPlaybackState;
+import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider;
+import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener;
+import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo;
+import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.util.Util;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** Handles loading and playback of a single ad tag. */
+/* package */ final class AdTagLoader implements Player.EventListener {
+
+ private static final String TAG = "AdTagLoader";
+
+ private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima";
+ private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION;
+
+ /**
+ * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is
+ * the interval recommended by the IMA documentation.
+ *
+ * @see VideoAdPlayer.VideoAdPlayerCallback
+ */
+ private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100;
+
+ /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */
+ private static final long IMA_DURATION_UNSET = -1L;
+
+ /**
+ * 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 THRESHOLD_END_OF_CONTENT_MS = 5000;
+ /**
+ * Threshold before the start of an ad at which IMA is expected to be able to preload the ad, in
+ * milliseconds.
+ */
+ 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;
+
+ /** The state of ad playback. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED})
+ private @interface ImaAdState {}
+
+ /** The ad playback state when IMA is not playing an ad. */
+ private static final int IMA_AD_STATE_NONE = 0;
+ /**
+ * The ad playback state when IMA has called {@link ComponentListener#playAd(AdMediaInfo)} and not
+ * {@link ComponentListener##pauseAd(AdMediaInfo)}.
+ */
+ private static final int IMA_AD_STATE_PLAYING = 1;
+ /**
+ * 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 final ImaUtil.Configuration configuration;
+ private final ImaUtil.ImaFactory imaFactory;
+ private final List supportedMimeTypes;
+ private final DataSpec adTagDataSpec;
+ private final Object adsId;
+ private final Timeline.Period period;
+ private final Handler handler;
+ private final ComponentListener componentListener;
+ private final List eventListeners;
+ private final List adCallbacks;
+ private final Runnable updateAdProgressRunnable;
+ private final BiMap adInfoByAdMediaInfo;
+ private final AdDisplayContainer adDisplayContainer;
+ private final AdsLoader adsLoader;
+
+ @Nullable private Object pendingAdRequestContext;
+ @Nullable private Player player;
+ private VideoProgressUpdate lastContentProgress;
+ private VideoProgressUpdate lastAdProgress;
+ private int lastVolumePercent;
+
+ @Nullable private AdsManager adsManager;
+ private boolean isAdsManagerInitialized;
+ @Nullable private AdLoadException pendingAdLoadError;
+ private Timeline timeline;
+ private long contentDurationMs;
+ private AdPlaybackState adPlaybackState;
+
+ private boolean released;
+
+ // Fields tracking IMA's state.
+
+ /** Whether IMA has sent an ad event to pause content since the last resume content event. */
+ private boolean imaPausedContent;
+ /** The current ad playback state. */
+ private @ImaAdState int imaAdState;
+ /** The current ad media info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. */
+ @Nullable private AdMediaInfo imaAdMediaInfo;
+ /** The current ad info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. */
+ @Nullable private AdInfo imaAdInfo;
+ /** Whether IMA has been notified that playback of content has finished. */
+ private boolean sentContentComplete;
+
+ // Fields tracking the player/loader state.
+
+ /** Whether the player is playing an ad. */
+ private boolean playingAd;
+ /** Whether the player is buffering an ad. */
+ private boolean bufferingAd;
+ /**
+ * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET}
+ * otherwise.
+ */
+ private int playingAdIndexInAdGroup;
+ /**
+ * The ad info for a pending ad for which the media failed preparation, or {@code null} if no
+ * pending ads have failed to prepare.
+ */
+ @Nullable private AdInfo pendingAdPrepareErrorAdInfo;
+ /**
+ * If a content period has finished but IMA has not yet called {@link
+ * ComponentListener#playAd(AdMediaInfo)}, stores the value of {@link
+ * SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to determine
+ * a fake, increasing content position. {@link C#TIME_UNSET} otherwise.
+ */
+ private long fakeContentProgressElapsedRealtimeMs;
+ /**
+ * If {@link #fakeContentProgressElapsedRealtimeMs} is set, stores the offset from which the
+ * content progress should increase. {@link C#TIME_UNSET} otherwise.
+ */
+ private long fakeContentProgressOffsetMs;
+ /** Stores the pending content position when a seek operation was intercepted to play an ad. */
+ private long pendingContentPositionMs;
+ /**
+ * Whether {@link ComponentListener#getContentProgress()} has sent {@link
+ * #pendingContentPositionMs} to IMA.
+ */
+ private boolean sentPendingContentPositionMs;
+ /**
+ * Stores the real time in milliseconds at which the player started buffering, possibly due to not
+ * having preloaded an ad, or {@link C#TIME_UNSET} if not applicable.
+ */
+ private long waitingForPreloadElapsedRealtimeMs;
+
+ /** Creates a new ad tag loader, starting the ad request if the ad tag is valid. */
+ @SuppressWarnings({"methodref.receiver.bound.invalid", "method.invocation.invalid"})
+ public AdTagLoader(
+ Context context,
+ ImaUtil.Configuration configuration,
+ ImaUtil.ImaFactory imaFactory,
+ List supportedMimeTypes,
+ DataSpec adTagDataSpec,
+ Object adsId,
+ @Nullable ViewGroup adViewGroup) {
+ this.configuration = configuration;
+ this.imaFactory = imaFactory;
+ @Nullable ImaSdkSettings imaSdkSettings = configuration.imaSdkSettings;
+ if (imaSdkSettings == null) {
+ imaSdkSettings = imaFactory.createImaSdkSettings();
+ if (configuration.debugModeEnabled) {
+ imaSdkSettings.setDebugMode(true);
+ }
+ }
+ imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
+ imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
+ this.supportedMimeTypes = supportedMimeTypes;
+ this.adTagDataSpec = adTagDataSpec;
+ this.adsId = adsId;
+ period = new Timeline.Period();
+ handler = Util.createHandler(getImaLooper(), /* callback= */ null);
+ componentListener = new ComponentListener();
+ eventListeners = new ArrayList<>();
+ adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
+ if (configuration.applicationVideoAdPlayerCallback != null) {
+ adCallbacks.add(configuration.applicationVideoAdPlayerCallback);
+ }
+ updateAdProgressRunnable = this::updateAdProgress;
+ adInfoByAdMediaInfo = HashBiMap.create();
+ lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
+ lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
+ fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
+ fakeContentProgressOffsetMs = C.TIME_UNSET;
+ pendingContentPositionMs = C.TIME_UNSET;
+ waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET;
+ contentDurationMs = C.TIME_UNSET;
+ timeline = Timeline.EMPTY;
+ adPlaybackState = AdPlaybackState.NONE;
+ if (adViewGroup != null) {
+ adDisplayContainer =
+ imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener);
+ } else {
+ adDisplayContainer =
+ imaFactory.createAudioAdDisplayContainer(context, /* player= */ componentListener);
+ }
+ if (configuration.companionAdSlots != null) {
+ adDisplayContainer.setCompanionSlots(configuration.companionAdSlots);
+ }
+ adsLoader = requestAds(context, imaSdkSettings, adDisplayContainer);
+ }
+
+ /** Returns the underlying IMA SDK ads loader. */
+ public AdsLoader getAdsLoader() {
+ return adsLoader;
+ }
+
+ /** Returns the IMA SDK ad display container. */
+ public AdDisplayContainer getAdDisplayContainer() {
+ return adDisplayContainer;
+ }
+
+ /** Skips the current skippable ad, if there is one. */
+ public void skipAd() {
+ if (adsManager != null) {
+ adsManager.skip();
+ }
+ }
+
+ /**
+ * Starts passing events from this instance (including any pending ad playback state) and
+ * registers obstructions.
+ */
+ public void addListenerWithAdView(EventListener eventListener, AdViewProvider adViewProvider) {
+ boolean isStarted = !eventListeners.isEmpty();
+ eventListeners.add(eventListener);
+ if (isStarted) {
+ if (!AdPlaybackState.NONE.equals(adPlaybackState)) {
+ // Pass the existing ad playback state to the new listener.
+ eventListener.onAdPlaybackState(adPlaybackState);
+ }
+ return;
+ }
+ lastVolumePercent = 0;
+ lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
+ lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
+ maybeNotifyPendingAdLoadError();
+ if (!AdPlaybackState.NONE.equals(adPlaybackState)) {
+ // Pass the ad playback state to the player, and resume ads if necessary.
+ eventListener.onAdPlaybackState(adPlaybackState);
+ } else if (adsManager != null) {
+ adPlaybackState =
+ new AdPlaybackState(adsId, getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints()));
+ updateAdPlaybackState();
+ }
+ for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) {
+ adDisplayContainer.registerFriendlyObstruction(
+ imaFactory.createFriendlyObstruction(
+ overlayInfo.view,
+ ImaUtil.getFriendlyObstructionPurpose(overlayInfo.purpose),
+ overlayInfo.reasonDetail));
+ }
+ }
+
+ /**
+ * Populates the ad playback state with loaded cue points, if available. Any preroll will be
+ * paused immediately while waiting for this instance to be {@link #activate(Player) activated}.
+ */
+ public void maybePreloadAds(long contentPositionMs, long contentDurationMs) {
+ maybeInitializeAdsManager(contentPositionMs, contentDurationMs);
+ }
+
+ /** Activates playback. */
+ public void activate(Player player) {
+ this.player = player;
+ player.addListener(this);
+
+ boolean playWhenReady = player.getPlayWhenReady();
+ onTimelineChanged(player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
+ if (!AdPlaybackState.NONE.equals(adPlaybackState)
+ && adsManager != null
+ && imaPausedContent
+ && playWhenReady) {
+ adsManager.resume();
+ }
+ }
+
+ /** Deactivates playback. */
+ public void deactivate() {
+ Player player = checkNotNull(this.player);
+ if (!AdPlaybackState.NONE.equals(adPlaybackState) && imaPausedContent) {
+ if (adsManager != null) {
+ adsManager.pause();
+ }
+ adPlaybackState =
+ adPlaybackState.withAdResumePositionUs(
+ playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
+ }
+ lastVolumePercent = getPlayerVolumePercent();
+ lastAdProgress = getAdVideoProgressUpdate();
+ lastContentProgress = getContentVideoProgressUpdate();
+
+ player.removeListener(this);
+ this.player = null;
+ }
+
+ /** Stops passing of events from this instance and unregisters obstructions. */
+ public void removeListener(EventListener eventListener) {
+ eventListeners.remove(eventListener);
+ if (eventListeners.isEmpty()) {
+ adDisplayContainer.unregisterAllFriendlyObstructions();
+ }
+ }
+
+ /** Releases all resources used by the ad tag loader. */
+ public void release() {
+ if (released) {
+ return;
+ }
+ released = true;
+ pendingAdRequestContext = null;
+ destroyAdsManager();
+ adsLoader.removeAdsLoadedListener(componentListener);
+ adsLoader.removeAdErrorListener(componentListener);
+ if (configuration.applicationAdErrorListener != null) {
+ adsLoader.removeAdErrorListener(configuration.applicationAdErrorListener);
+ }
+ adsLoader.release();
+ imaPausedContent = false;
+ imaAdState = IMA_AD_STATE_NONE;
+ imaAdMediaInfo = null;
+ stopUpdatingAdProgress();
+ imaAdInfo = null;
+ pendingAdLoadError = null;
+ adPlaybackState = new AdPlaybackState(adsId);
+ updateAdPlaybackState();
+ }
+
+ /** Notifies the IMA SDK that the specified ad has been prepared for playback. */
+ public void handlePrepareComplete(int adGroupIndex, int adIndexInAdGroup) {
+ AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
+ if (configuration.debugModeEnabled) {
+ Log.d(TAG, "Prepared ad " + adInfo);
+ }
+ @Nullable AdMediaInfo adMediaInfo = adInfoByAdMediaInfo.inverse().get(adInfo);
+ if (adMediaInfo != null) {
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onLoaded(adMediaInfo);
+ }
+ } else {
+ Log.w(TAG, "Unexpected prepared ad " + adInfo);
+ }
+ }
+
+ /** Notifies the IMA SDK that the specified ad has failed to prepare for playback. */
+ public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) {
+ if (player == null) {
+ return;
+ }
+ try {
+ handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception);
+ } catch (RuntimeException e) {
+ maybeNotifyInternalError("handlePrepareError", e);
+ }
+ }
+
+ // Player.EventListener implementation.
+
+ @Override
+ public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
+ if (timeline.isEmpty()) {
+ // The player is being reset or contains no media.
+ return;
+ }
+ this.timeline = timeline;
+ Player player = checkNotNull(this.player);
+ long contentDurationUs = timeline.getPeriod(player.getCurrentPeriodIndex(), period).durationUs;
+ contentDurationMs = C.usToMs(contentDurationUs);
+ if (contentDurationUs != adPlaybackState.contentDurationUs) {
+ adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);
+ updateAdPlaybackState();
+ }
+ long contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
+ maybeInitializeAdsManager(contentPositionMs, contentDurationMs);
+ handleTimelineOrPositionChanged();
+ }
+
+ @Override
+ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ handleTimelineOrPositionChanged();
+ }
+
+ @Override
+ public void onPlaybackStateChanged(@Player.State int playbackState) {
+ @Nullable Player player = this.player;
+ if (adsManager == null || player == null) {
+ return;
+ }
+
+ if (playbackState == Player.STATE_BUFFERING
+ && !player.isPlayingAd()
+ && isWaitingForAdToLoad()) {
+ waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime();
+ } else if (playbackState == Player.STATE_READY) {
+ waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET;
+ }
+
+ handlePlayerStateChanged(player.getPlayWhenReady(), playbackState);
+ }
+
+ @Override
+ public void onPlayWhenReadyChanged(
+ boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
+ if (adsManager == null || player == null) {
+ return;
+ }
+
+ if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) {
+ adsManager.pause();
+ return;
+ }
+
+ if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) {
+ adsManager.resume();
+ return;
+ }
+ handlePlayerStateChanged(playWhenReady, player.getPlaybackState());
+ }
+
+ @Override
+ public void onPlayerError(ExoPlaybackException error) {
+ if (imaAdState != IMA_AD_STATE_NONE) {
+ AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onError(adMediaInfo);
+ }
+ }
+ }
+
+ // Internal methods.
+
+ private AdsLoader requestAds(
+ Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
+ AdsLoader adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
+ adsLoader.addAdErrorListener(componentListener);
+ if (configuration.applicationAdErrorListener != null) {
+ adsLoader.addAdErrorListener(configuration.applicationAdErrorListener);
+ }
+ adsLoader.addAdsLoadedListener(componentListener);
+ AdsRequest request;
+ try {
+ request = ImaUtil.getAdsRequestForAdTagDataSpec(imaFactory, adTagDataSpec);
+ } catch (IOException e) {
+ adPlaybackState = new AdPlaybackState(adsId);
+ updateAdPlaybackState();
+ pendingAdLoadError = AdLoadException.createForAllAds(e);
+ maybeNotifyPendingAdLoadError();
+ return adsLoader;
+ }
+ pendingAdRequestContext = new Object();
+ request.setUserRequestContext(pendingAdRequestContext);
+ if (configuration.enableContinuousPlayback != null) {
+ request.setContinuousPlayback(configuration.enableContinuousPlayback);
+ }
+ if (configuration.vastLoadTimeoutMs != TIMEOUT_UNSET) {
+ request.setVastLoadTimeout(configuration.vastLoadTimeoutMs);
+ }
+ request.setContentProgressProvider(componentListener);
+ adsLoader.requestAds(request);
+ return adsLoader;
+ }
+
+ private void maybeInitializeAdsManager(long contentPositionMs, long contentDurationMs) {
+ @Nullable AdsManager adsManager = this.adsManager;
+ if (!isAdsManagerInitialized && adsManager != null) {
+ isAdsManagerInitialized = true;
+ @Nullable
+ AdsRenderingSettings adsRenderingSettings =
+ setupAdsRendering(contentPositionMs, contentDurationMs);
+ if (adsRenderingSettings == null) {
+ // There are no ads to play.
+ destroyAdsManager();
+ } else {
+ adsManager.init(adsRenderingSettings);
+ adsManager.start();
+ if (configuration.debugModeEnabled) {
+ Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings);
+ }
+ }
+ updateAdPlaybackState();
+ }
+ }
+
+ /**
+ * 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(long contentPositionMs, long contentDurationMs) {
+ AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
+ adsRenderingSettings.setEnablePreloading(true);
+ adsRenderingSettings.setMimeTypes(
+ configuration.adMediaMimeTypes != null
+ ? configuration.adMediaMimeTypes
+ : supportedMimeTypes);
+ if (configuration.mediaLoadTimeoutMs != TIMEOUT_UNSET) {
+ adsRenderingSettings.setLoadVideoTimeout(configuration.mediaLoadTimeoutMs);
+ }
+ if (configuration.mediaBitrate != BITRATE_UNSET) {
+ adsRenderingSettings.setBitrateKbps(configuration.mediaBitrate / 1000);
+ }
+ adsRenderingSettings.setFocusSkipButtonWhenAvailable(
+ configuration.focusSkipButtonWhenAvailable);
+ if (configuration.adUiElements != null) {
+ adsRenderingSettings.setUiElements(configuration.adUiElements);
+ }
+
+ // Skip ads based on the start position as required.
+ long[] adGroupTimesUs = adPlaybackState.adGroupTimesUs;
+ int adGroupForPositionIndex =
+ adPlaybackState.getAdGroupIndexForPositionUs(
+ C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
+ if (adGroupForPositionIndex != C.INDEX_UNSET) {
+ boolean playAdWhenStartingPlayback =
+ configuration.playAdBeforeStartPosition
+ || 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);
+ }
+ }
+ }
+ return adsRenderingSettings;
+ }
+
+ private VideoProgressUpdate getContentVideoProgressUpdate() {
+ boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
+ long contentPositionMs;
+ if (pendingContentPositionMs != C.TIME_UNSET) {
+ sentPendingContentPositionMs = true;
+ contentPositionMs = pendingContentPositionMs;
+ } else if (player == null) {
+ return lastContentProgress;
+ } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
+ long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
+ contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
+ } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
+ contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
+ } else {
+ return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
+ }
+ long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
+ return new VideoProgressUpdate(contentPositionMs, contentDurationMs);
+ }
+
+ private VideoProgressUpdate getAdVideoProgressUpdate() {
+ if (player == null) {
+ return lastAdProgress;
+ } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) {
+ long adDuration = player.getDuration();
+ return adDuration == C.TIME_UNSET
+ ? VideoProgressUpdate.VIDEO_TIME_NOT_READY
+ : new VideoProgressUpdate(player.getCurrentPosition(), adDuration);
+ } else {
+ return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
+ }
+ }
+
+ private void updateAdProgress() {
+ VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate();
+ if (configuration.debugModeEnabled) {
+ Log.d(TAG, "Ad progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate));
+ }
+
+ AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate);
+ }
+ handler.removeCallbacks(updateAdProgressRunnable);
+ handler.postDelayed(updateAdProgressRunnable, AD_PROGRESS_UPDATE_INTERVAL_MS);
+ }
+
+ private void stopUpdatingAdProgress() {
+ 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 handleAdEvent(AdEvent adEvent) {
+ if (adsManager == null) {
+ // Drop events after release.
+ return;
+ }
+ switch (adEvent.getType()) {
+ case AD_BREAK_FETCH_ERROR:
+ String adGroupTimeSecondsString = checkNotNull(adEvent.getAdData().get("adBreakTime"));
+ if (configuration.debugModeEnabled) {
+ Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds");
+ }
+ double adGroupTimeSeconds = Double.parseDouble(adGroupTimeSecondsString);
+ int adGroupIndex =
+ adGroupTimeSeconds == -1.0
+ ? adPlaybackState.adGroupCount - 1
+ : getAdGroupIndexForCuePointTimeSeconds(adGroupTimeSeconds);
+ markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex);
+ break;
+ case CONTENT_PAUSE_REQUESTED:
+ // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
+ // before sending CONTENT_RESUME_REQUESTED.
+ imaPausedContent = true;
+ pauseContentInternal();
+ break;
+ case TAPPED:
+ for (int i = 0; i < eventListeners.size(); i++) {
+ eventListeners.get(i).onAdTapped();
+ }
+ break;
+ case CLICKED:
+ for (int i = 0; i < eventListeners.size(); i++) {
+ eventListeners.get(i).onAdClicked();
+ }
+ break;
+ case CONTENT_RESUME_REQUESTED:
+ imaPausedContent = false;
+ resumeContentInternal();
+ break;
+ case LOG:
+ Map adData = adEvent.getAdData();
+ String message = "AdEvent: " + adData;
+ Log.i(TAG, message);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void pauseContentInternal() {
+ imaAdState = IMA_AD_STATE_NONE;
+ if (sentPendingContentPositionMs) {
+ pendingContentPositionMs = C.TIME_UNSET;
+ sentPendingContentPositionMs = false;
+ }
+ }
+
+ private void resumeContentInternal() {
+ if (imaAdInfo != null) {
+ adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex);
+ updateAdPlaybackState();
+ }
+ }
+
+ /**
+ * Returns whether this instance is expecting the first ad in an the upcoming ad group to load
+ * within the {@link ImaUtil.Configuration#adPreloadTimeoutMs preload timeout}.
+ */
+ private boolean isWaitingForAdToLoad() {
+ @Nullable Player player = this.player;
+ if (player == null) {
+ return false;
+ }
+ int adGroupIndex = getLoadingAdGroupIndex();
+ if (adGroupIndex == C.INDEX_UNSET) {
+ return false;
+ }
+ AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
+ if (adGroup.count != C.LENGTH_UNSET
+ && adGroup.count != 0
+ && adGroup.states[0] != AdPlaybackState.AD_STATE_UNAVAILABLE) {
+ // An ad is available already.
+ return false;
+ }
+ long adGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
+ long contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
+ long timeUntilAdMs = adGroupTimeMs - contentPositionMs;
+ return timeUntilAdMs < configuration.adPreloadTimeoutMs;
+ }
+
+ private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
+ if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) {
+ if (!bufferingAd && playbackState == Player.STATE_BUFFERING) {
+ bufferingAd = true;
+ AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onBuffering(adMediaInfo);
+ }
+ stopUpdatingAdProgress();
+ } else if (bufferingAd && playbackState == Player.STATE_READY) {
+ bufferingAd = false;
+ updateAdProgress();
+ }
+ }
+
+ if (imaAdState == IMA_AD_STATE_NONE
+ && playbackState == Player.STATE_BUFFERING
+ && playWhenReady) {
+ ensureSentContentCompleteIfAtEndOfStream();
+ } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) {
+ @Nullable AdMediaInfo adMediaInfo = imaAdMediaInfo;
+ if (adMediaInfo == null) {
+ Log.w(TAG, "onEnded without ad media info");
+ } else {
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onEnded(adMediaInfo);
+ }
+ }
+ if (configuration.debugModeEnabled) {
+ Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged");
+ }
+ }
+ }
+
+ private void handleTimelineOrPositionChanged() {
+ @Nullable Player player = this.player;
+ if (adsManager == null || player == null) {
+ return;
+ }
+ if (!playingAd && !player.isPlayingAd()) {
+ ensureSentContentCompleteIfAtEndOfStream();
+ if (!sentContentComplete && !timeline.isEmpty()) {
+ long positionMs = getContentPeriodPositionMs(player, timeline, period);
+ timeline.getPeriod(/* periodIndex= */ 0, period);
+ int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs));
+ if (newAdGroupIndex != C.INDEX_UNSET) {
+ sentPendingContentPositionMs = false;
+ pendingContentPositionMs = positionMs;
+ }
+ }
+ }
+
+ boolean wasPlayingAd = playingAd;
+ int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup;
+ playingAd = player.isPlayingAd();
+ playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET;
+ boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup;
+ if (adFinished) {
+ // IMA is waiting for the ad playback to finish so invoke the callback now.
+ // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
+ @Nullable AdMediaInfo adMediaInfo = imaAdMediaInfo;
+ if (adMediaInfo == null) {
+ Log.w(TAG, "onEnded without ad media info");
+ } else {
+ @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo);
+ if (playingAdIndexInAdGroup == C.INDEX_UNSET
+ || (adInfo != null && adInfo.adIndexInAdGroup < playingAdIndexInAdGroup)) {
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onEnded(adMediaInfo);
+ }
+ if (configuration.debugModeEnabled) {
+ Log.d(
+ TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
+ }
+ }
+ }
+ }
+ if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) {
+ int adGroupIndex = player.getCurrentAdGroupIndex();
+ if (adPlaybackState.adGroupTimesUs[adGroupIndex] == C.TIME_END_OF_SOURCE) {
+ sendContentComplete();
+ } 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;
+ }
+ }
+ }
+ }
+
+ private void loadAdInternal(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) {
+ if (adsManager == null) {
+ // Drop events after release.
+ if (configuration.debugModeEnabled) {
+ 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 (configuration.debugModeEnabled) {
+ 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, 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();
+ }
+
+ private void playAdInternal(AdMediaInfo adMediaInfo) {
+ if (configuration.debugModeEnabled) {
+ 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 = 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;
+ checkState(adMediaInfo.equals(imaAdMediaInfo));
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onResume(adMediaInfo);
+ }
+ }
+ if (player == null || !player.getPlayWhenReady()) {
+ // Either this loader hasn't been activated yet, or the player is paused now.
+ checkNotNull(adsManager).pause();
+ }
+ }
+
+ private void pauseAdInternal(AdMediaInfo adMediaInfo) {
+ if (configuration.debugModeEnabled) {
+ 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;
+ }
+ if (configuration.debugModeEnabled && !adMediaInfo.equals(imaAdMediaInfo)) {
+ Log.w(
+ TAG,
+ "Unexpected pauseAd for "
+ + getAdMediaInfoString(adMediaInfo)
+ + ", expected "
+ + getAdMediaInfoString(imaAdMediaInfo));
+ }
+ imaAdState = IMA_AD_STATE_PAUSED;
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onPause(adMediaInfo);
+ }
+ }
+
+ private void stopAdInternal(AdMediaInfo adMediaInfo) {
+ if (configuration.debugModeEnabled) {
+ 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;
+ }
+ imaAdState = IMA_AD_STATE_NONE;
+ stopUpdatingAdProgress();
+ // TODO: Handle the skipped event so the ad can be marked as skipped rather than played.
+ checkNotNull(imaAdInfo);
+ int adGroupIndex = imaAdInfo.adGroupIndex;
+ int adIndexInAdGroup = imaAdInfo.adIndexInAdGroup;
+ if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) {
+ // We have already marked this ad as having failed to load, so ignore the request.
+ return;
+ }
+ adPlaybackState =
+ adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0);
+ updateAdPlaybackState();
+ if (!playingAd) {
+ imaAdMediaInfo = null;
+ imaAdInfo = null;
+ }
+ }
+
+ private void handleAdGroupLoadError(Exception error) {
+ int adGroupIndex = getLoadingAdGroupIndex();
+ if (adGroupIndex == C.INDEX_UNSET) {
+ Log.w(TAG, "Unable to determine ad group index for ad group load error", error);
+ return;
+ }
+ markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex);
+ if (pendingAdLoadError == null) {
+ pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
+ }
+ }
+
+ private void markAdGroupInErrorStateAndClearPendingContentPosition(int adGroupIndex) {
+ // Update the ad playback state so all ads in the ad group are in the error state.
+ AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
+ if (adGroup.count == C.LENGTH_UNSET) {
+ adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, max(1, adGroup.states.length));
+ adGroup = adPlaybackState.adGroups[adGroupIndex];
+ }
+ for (int i = 0; i < adGroup.count; i++) {
+ if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
+ if (configuration.debugModeEnabled) {
+ Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex);
+ }
+ adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);
+ }
+ }
+ updateAdPlaybackState();
+ // Clear any pending content position that triggered attempting to load the ad group.
+ pendingContentPositionMs = C.TIME_UNSET;
+ fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
+ }
+
+ private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
+ if (configuration.debugModeEnabled) {
+ Log.d(
+ TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
+ }
+ if (adsManager == null) {
+ Log.w(TAG, "Ignoring ad prepare error after release");
+ return;
+ }
+ if (imaAdState == IMA_AD_STATE_NONE) {
+ // Send IMA a content position at the ad group so that it will try to play it, at which point
+ // we can notify that it failed to load.
+ fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
+ fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
+ if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
+ fakeContentProgressOffsetMs = contentDurationMs;
+ }
+ pendingAdPrepareErrorAdInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
+ } else {
+ AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
+ // We're already playing an ad.
+ if (adIndexInAdGroup > playingAdIndexInAdGroup) {
+ // Mark the playing ad as ended so we can notify the error on the next ad and remove it,
+ // which means that the ad after will load (if any).
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onEnded(adMediaInfo);
+ }
+ }
+ playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onError(checkNotNull(adMediaInfo));
+ }
+ }
+ adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup);
+ updateAdPlaybackState();
+ }
+
+ private void ensureSentContentCompleteIfAtEndOfStream() {
+ if (!sentContentComplete
+ && contentDurationMs != C.TIME_UNSET
+ && pendingContentPositionMs == C.TIME_UNSET
+ && getContentPeriodPositionMs(checkNotNull(player), timeline, period)
+ + THRESHOLD_END_OF_CONTENT_MS
+ >= contentDurationMs) {
+ sendContentComplete();
+ }
+ }
+
+ private void sendContentComplete() {
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onContentComplete();
+ }
+ sentContentComplete = true;
+ if (configuration.debugModeEnabled) {
+ Log.d(TAG, "adsLoader.contentComplete");
+ }
+ for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
+ if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) {
+ adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ i);
+ }
+ }
+ updateAdPlaybackState();
+ }
+
+ private void updateAdPlaybackState() {
+ for (int i = 0; i < eventListeners.size(); i++) {
+ eventListeners.get(i).onAdPlaybackState(adPlaybackState);
+ }
+ }
+
+ private void maybeNotifyPendingAdLoadError() {
+ if (pendingAdLoadError != null) {
+ for (int i = 0; i < eventListeners.size(); i++) {
+ eventListeners.get(i).onAdLoadError(pendingAdLoadError, adTagDataSpec);
+ }
+ pendingAdLoadError = null;
+ }
+ }
+
+ private void maybeNotifyInternalError(String name, Exception cause) {
+ 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.
+ for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
+ adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
+ }
+ updateAdPlaybackState();
+ for (int i = 0; i < eventListeners.size(); i++) {
+ eventListeners
+ .get(i)
+ .onAdLoadError(
+ AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
+ adTagDataSpec);
+ }
+ }
+
+ private int getAdGroupIndexForAdPod(AdPodInfo adPodInfo) {
+ if (adPodInfo.getPodIndex() == -1) {
+ // This is a postroll ad.
+ return adPlaybackState.adGroupCount - 1;
+ }
+
+ // adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead.
+ return getAdGroupIndexForCuePointTimeSeconds(adPodInfo.getTimeOffset());
+ }
+
+ /**
+ * Returns the index of the ad group that will preload next, or {@link C#INDEX_UNSET} if there is
+ * no such ad group.
+ */
+ private int getLoadingAdGroupIndex() {
+ if (player == null) {
+ return C.INDEX_UNSET;
+ }
+ long playerPositionUs = C.msToUs(getContentPeriodPositionMs(player, timeline, period));
+ int adGroupIndex =
+ adPlaybackState.getAdGroupIndexForPositionUs(playerPositionUs, C.msToUs(contentDurationMs));
+ if (adGroupIndex == C.INDEX_UNSET) {
+ adGroupIndex =
+ adPlaybackState.getAdGroupIndexAfterPositionUs(
+ playerPositionUs, C.msToUs(contentDurationMs));
+ }
+ return adGroupIndex;
+ }
+
+ private int getAdGroupIndexForCuePointTimeSeconds(double cuePointTimeSeconds) {
+ // We receive initial 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).
+ float cuePointTimeSecondsFloat = (float) cuePointTimeSeconds;
+ long adPodTimeUs = Math.round((double) cuePointTimeSecondsFloat * C.MICROS_PER_SECOND);
+ for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) {
+ long adGroupTimeUs = adPlaybackState.adGroupTimesUs[adGroupIndex];
+ if (adGroupTimeUs != C.TIME_END_OF_SOURCE
+ && Math.abs(adGroupTimeUs - adPodTimeUs) < THRESHOLD_AD_MATCH_US) {
+ return adGroupIndex;
+ }
+ }
+ throw new IllegalStateException("Failed to find cue point");
+ }
+
+ private String getAdMediaInfoString(@Nullable AdMediaInfo adMediaInfo) {
+ @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo);
+ return "AdMediaInfo["
+ + (adMediaInfo == null ? "null" : adMediaInfo.getUrl())
+ + ", "
+ + adInfo
+ + "]";
+ }
+
+ private static long getContentPeriodPositionMs(
+ Player player, Timeline timeline, Timeline.Period period) {
+ long contentWindowPositionMs = player.getContentPosition();
+ if (timeline.isEmpty()) {
+ return contentWindowPositionMs;
+ } else {
+ return contentWindowPositionMs
+ - timeline.getPeriod(player.getCurrentPeriodIndex(), period).getPositionInWindowMs();
+ }
+ }
+
+ private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) {
+ int count = adGroupTimesUs.length;
+ if (count == 1) {
+ return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE;
+ } else if (count == 2) {
+ return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE;
+ } else {
+ // There's at least one midroll ad group, as adGroupTimesUs is never empty.
+ return true;
+ }
+ }
+
+ private void destroyAdsManager() {
+ if (adsManager != null) {
+ adsManager.removeAdErrorListener(componentListener);
+ if (configuration.applicationAdErrorListener != null) {
+ adsManager.removeAdErrorListener(configuration.applicationAdErrorListener);
+ }
+ adsManager.removeAdEventListener(componentListener);
+ if (configuration.applicationAdEventListener != null) {
+ adsManager.removeAdEventListener(configuration.applicationAdEventListener);
+ }
+ adsManager.destroy();
+ adsManager = null;
+ }
+ }
+
+ private final class ComponentListener
+ implements AdsLoadedListener,
+ ContentProgressProvider,
+ AdEventListener,
+ AdErrorListener,
+ VideoAdPlayer {
+
+ // AdsLoader.AdsLoadedListener implementation.
+
+ @Override
+ public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
+ AdsManager adsManager = adsManagerLoadedEvent.getAdsManager();
+ if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) {
+ adsManager.destroy();
+ return;
+ }
+ pendingAdRequestContext = null;
+ AdTagLoader.this.adsManager = adsManager;
+ adsManager.addAdErrorListener(this);
+ if (configuration.applicationAdErrorListener != null) {
+ adsManager.addAdErrorListener(configuration.applicationAdErrorListener);
+ }
+ adsManager.addAdEventListener(this);
+ if (configuration.applicationAdEventListener != null) {
+ adsManager.addAdEventListener(configuration.applicationAdEventListener);
+ }
+ try {
+ adPlaybackState =
+ new AdPlaybackState(adsId, getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints()));
+ updateAdPlaybackState();
+ } catch (RuntimeException e) {
+ maybeNotifyInternalError("onAdsManagerLoaded", e);
+ }
+ }
+
+ // ContentProgressProvider implementation.
+
+ @Override
+ public VideoProgressUpdate getContentProgress() {
+ VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
+ if (configuration.debugModeEnabled) {
+ Log.d(
+ TAG,
+ "Content progress: " + ImaUtil.getStringForVideoProgressUpdate(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();
+ }
+ } else if (pendingContentPositionMs != C.TIME_UNSET
+ && player != null
+ && player.getPlaybackState() == Player.STATE_BUFFERING
+ && isWaitingForAdToLoad()) {
+ // Prepare to timeout the load of an ad for the pending seek operation.
+ waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime();
+ }
+
+ return videoProgressUpdate;
+ }
+
+ // AdEvent.AdEventListener implementation.
+
+ @Override
+ public void onAdEvent(AdEvent adEvent) {
+ AdEventType adEventType = adEvent.getType();
+ if (configuration.debugModeEnabled && adEventType != AdEventType.AD_PROGRESS) {
+ Log.d(TAG, "onAdEvent: " + adEventType);
+ }
+ try {
+ handleAdEvent(adEvent);
+ } catch (RuntimeException e) {
+ maybeNotifyInternalError("onAdEvent", e);
+ }
+ }
+
+ // AdErrorEvent.AdErrorListener implementation.
+
+ @Override
+ public void onAdError(AdErrorEvent adErrorEvent) {
+ AdError error = adErrorEvent.getError();
+ if (configuration.debugModeEnabled) {
+ Log.d(TAG, "onAdError", error);
+ }
+ if (adsManager == null) {
+ // No ads were loaded, so allow playback to start without any ads.
+ pendingAdRequestContext = null;
+ adPlaybackState = new AdPlaybackState(adsId);
+ updateAdPlaybackState();
+ } else if (ImaUtil.isAdGroupLoadError(error)) {
+ try {
+ handleAdGroupLoadError(error);
+ } catch (RuntimeException e) {
+ maybeNotifyInternalError("onAdError", e);
+ }
+ }
+ if (pendingAdLoadError == null) {
+ pendingAdLoadError = AdLoadException.createForAllAds(error);
+ }
+ maybeNotifyPendingAdLoadError();
+ }
+
+ // VideoAdPlayer implementation.
+
+ @Override
+ public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
+ adCallbacks.add(videoAdPlayerCallback);
+ }
+
+ @Override
+ public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) {
+ adCallbacks.remove(videoAdPlayerCallback);
+ }
+
+ @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 {
+ loadAdInternal(adMediaInfo, adPodInfo);
+ } catch (RuntimeException e) {
+ maybeNotifyInternalError("loadAd", e);
+ }
+ }
+
+ @Override
+ public void playAd(AdMediaInfo adMediaInfo) {
+ try {
+ playAdInternal(adMediaInfo);
+ } catch (RuntimeException e) {
+ maybeNotifyInternalError("playAd", e);
+ }
+ }
+
+ @Override
+ public void pauseAd(AdMediaInfo adMediaInfo) {
+ try {
+ pauseAdInternal(adMediaInfo);
+ } catch (RuntimeException e) {
+ maybeNotifyInternalError("pauseAd", e);
+ }
+ }
+
+ @Override
+ public void stopAd(AdMediaInfo adMediaInfo) {
+ try {
+ stopAdInternal(adMediaInfo);
+ } catch (RuntimeException e) {
+ maybeNotifyInternalError("stopAd", e);
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing.
+ }
+ }
+
+ // TODO: Consider moving this into AdPlaybackState.
+ private static final class AdInfo {
+
+ public final int adGroupIndex;
+ public final int adIndexInAdGroup;
+
+ public AdInfo(int adGroupIndex, int adIndexInAdGroup) {
+ this.adGroupIndex = adGroupIndex;
+ this.adIndexInAdGroup = adIndexInAdGroup;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ AdInfo adInfo = (AdInfo) o;
+ if (adGroupIndex != adInfo.adGroupIndex) {
+ return false;
+ }
+ return adIndexInAdGroup == adInfo.adIndexInAdGroup;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = adGroupIndex;
+ result = 31 * result + adIndexInAdGroup;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "(" + adGroupIndex + ", " + adIndexInAdGroup + ')';
+ }
+ }
+}
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 574fbfa9175..e2adbaf2d02 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
@@ -15,33 +15,23 @@
*/
package com.google.android.exoplayer2.ext.ima;
+import static com.google.android.exoplayer2.ext.ima.ImaUtil.BITRATE_UNSET;
+import static com.google.android.exoplayer2.ext.ima.ImaUtil.TIMEOUT_UNSET;
+import static com.google.android.exoplayer2.ext.ima.ImaUtil.getImaLooper;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
-import static java.lang.Math.max;
import android.content.Context;
-import android.net.Uri;
-import android.os.Handler;
import android.os.Looper;
-import android.os.SystemClock;
import android.view.View;
import android.view.ViewGroup;
-import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
-import com.google.ads.interactivemedia.v3.api.AdError;
-import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
-import com.google.ads.interactivemedia.v3.api.AdEvent;
import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener;
-import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType;
-import com.google.ads.interactivemedia.v3.api.AdPodInfo;
-import com.google.ads.interactivemedia.v3.api.AdsLoader;
-import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener;
import com.google.ads.interactivemedia.v3.api.AdsManager;
-import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.CompanionAdSlot;
@@ -50,45 +40,30 @@
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.UiElement;
-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.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSourceFactory;
-import com.google.android.exoplayer2.source.ads.AdPlaybackState;
+import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
-import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSpec;
-import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-import java.util.Locale;
import java.util.Set;
-import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
- * {@link com.google.android.exoplayer2.source.ads.AdsLoader} using the IMA SDK. All methods must be
- * called on the main thread.
+ * {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread.
*
*
The player instance that will play the loaded ads must be set before playback using {@link
* #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
@@ -109,8 +84,7 @@
* href="https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/omsdk">IMA
* SDK Open Measurement documentation for more information.
*/
-public final class ImaAdsLoader
- implements Player.EventListener, com.google.android.exoplayer2.source.ads.AdsLoader {
+public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
@@ -139,6 +113,7 @@ public static final class Builder {
@Nullable private List adMediaMimeTypes;
@Nullable private Set adUiElements;
@Nullable private Collection companionAdSlots;
+ @Nullable private Boolean enableContinuousPlayback;
private long adPreloadTimeoutMs;
private int vastLoadTimeoutMs;
private int mediaLoadTimeoutMs;
@@ -180,8 +155,8 @@ public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) {
/**
* Sets a listener for ad errors that will be passed to {@link
- * AdsLoader#addAdErrorListener(AdErrorListener)} and {@link
- * AdsManager#addAdErrorListener(AdErrorListener)}.
+ * com.google.ads.interactivemedia.v3.api.AdsLoader#addAdErrorListener(AdErrorListener)} and
+ * {@link AdsManager#addAdErrorListener(AdErrorListener)}.
*
* @param adErrorListener The ad error listener.
* @return This builder, for convenience.
@@ -260,6 +235,20 @@ public Builder setAdMediaMimeTypes(List adMediaMimeTypes) {
return this;
}
+ /**
+ * Sets whether to enable continuous playback. Pass {@code true} if content videos will be
+ * played continuously, similar to a TV broadcast. This setting may modify the ads request but
+ * does not affect ad playback behavior. The requested value is unknown by default.
+ *
+ * @param enableContinuousPlayback Whether to enable continuous playback.
+ * @return This builder, for convenience.
+ * @see AdsRequest#setContinuousPlayback(boolean)
+ */
+ public Builder setEnableContinuousPlayback(boolean enableContinuousPlayback) {
+ this.enableContinuousPlayback = enableContinuousPlayback;
+ return this;
+ }
+
/**
* Sets the duration in milliseconds for which the player must buffer while preloading an ad
* group before that ad group is skipped and marked as having failed to load. Pass {@link
@@ -368,285 +357,63 @@ public Builder setDebugModeEnabled(boolean debugModeEnabled) {
return this;
}
- /**
- * Returns a new {@link ImaAdsLoader} for the specified ad tag.
- *
- * @param adTagUri The URI of a compatible ad tag to load. See
- * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
- * information on compatible ad tags.
- * @return The new {@link ImaAdsLoader}.
- * @deprecated Pass the ad tag URI when setting media item playback properties (if using the
- * media item API) or as a {@link DataSpec} when constructing the {@link AdsMediaSource} (if
- * using media sources directly).
- */
- @Deprecated
- public ImaAdsLoader buildForAdTag(Uri adTagUri) {
- return new ImaAdsLoader(
- context,
- getConfiguration(),
- imaFactory,
- /* adTagUri= */ adTagUri,
- /* adsResponse= */ null);
- }
-
- /**
- * Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response.
- *
- * @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of
- * making a request via an ad tag URL.
- * @return The new {@link ImaAdsLoader}.
- * @deprecated Pass the ads response as a data URI when setting media item playback properties
- * (if using the media item API) or as a {@link DataSpec} when constructing the {@link
- * AdsMediaSource} (if using media sources directly). {@link
- * Util#getDataUriForString(String, String)} can be used to construct a data URI from
- * literal string ads response (with MIME type text/xml).
- */
- @Deprecated
- public ImaAdsLoader buildForAdsResponse(String adsResponse) {
- return new ImaAdsLoader(
- context, getConfiguration(), imaFactory, /* adTagUri= */ null, adsResponse);
- }
-
/** Returns a new {@link ImaAdsLoader}. */
public ImaAdsLoader build() {
return new ImaAdsLoader(
- context, getConfiguration(), imaFactory, /* adTagUri= */ null, /* adsResponse= */ null);
- }
-
- // TODO(internal: b/169646419): Remove/hide once the deprecated constructor has been removed.
- /* package */ ImaUtil.Configuration getConfiguration() {
- return new ImaUtil.Configuration(
- adPreloadTimeoutMs,
- vastLoadTimeoutMs,
- mediaLoadTimeoutMs,
- focusSkipButtonWhenAvailable,
- playAdBeforeStartPosition,
- mediaBitrate,
- adMediaMimeTypes,
- adUiElements,
- companionAdSlots,
- adErrorListener,
- adEventListener,
- videoAdPlayerCallback,
- imaSdkSettings,
- debugModeEnabled);
+ context,
+ new ImaUtil.Configuration(
+ adPreloadTimeoutMs,
+ vastLoadTimeoutMs,
+ mediaLoadTimeoutMs,
+ focusSkipButtonWhenAvailable,
+ playAdBeforeStartPosition,
+ mediaBitrate,
+ enableContinuousPlayback,
+ adMediaMimeTypes,
+ adUiElements,
+ companionAdSlots,
+ adErrorListener,
+ adEventListener,
+ videoAdPlayerCallback,
+ imaSdkSettings,
+ debugModeEnabled),
+ imaFactory);
}
}
- private static final String TAG = "ImaAdsLoader";
-
- private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima";
- private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION;
-
- /**
- * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is
- * the interval recommended by the IMA documentation.
- *
- * @see VideoAdPlayer.VideoAdPlayerCallback
- */
- private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100;
-
- /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */
- private static final long IMA_DURATION_UNSET = -1L;
-
- /**
- * 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 THRESHOLD_END_OF_CONTENT_MS = 5000;
- /**
- * Threshold before the start of an ad at which IMA is expected to be able to preload the ad, in
- * milliseconds.
- */
- 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 BITRATE_UNSET = -1;
-
- /** The state of ad playback. */
- @Documented
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED})
- private @interface ImaAdState {}
- /** The ad playback state when IMA is not playing an ad. */
- private static final int IMA_AD_STATE_NONE = 0;
- /**
- * The ad playback state when IMA has called {@link ComponentListener#playAd(AdMediaInfo)} and not
- * {@link ComponentListener##pauseAd(AdMediaInfo)}.
- */
- private static final int IMA_AD_STATE_PLAYING = 1;
- /**
- * 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 DataSpec EMPTY_AD_TAG_DATA_SPEC = new DataSpec(Uri.EMPTY);
-
private final ImaUtil.Configuration configuration;
private final Context context;
private final ImaUtil.ImaFactory imaFactory;
- @Nullable private final Uri adTagUri;
- @Nullable private final String adsResponse;
- private final ImaSdkSettings imaSdkSettings;
+ private final HashMap