diff --git a/.hgignore b/.hgignore
index e2c54bcf61..5681fe9625 100644
--- a/.hgignore
+++ b/.hgignore
@@ -75,9 +75,3 @@ extensions/flac/src/main/jni/flac
# FFmpeg extension
extensions/ffmpeg/src/main/jni/ffmpeg
-
-# Cronet extension
-extensions/cronet/jniLibs/*
-!extensions/cronet/jniLibs/README.md
-extensions/cronet/libs/*
-!extensions/cronet/libs/README.md
diff --git a/README.md b/README.md
index 73f57c92a1..856f961ae9 100644
--- a/README.md
+++ b/README.md
@@ -104,10 +104,10 @@ git checkout release-v2
```
Next, add the following to your project's `settings.gradle` file, replacing
-`path/to/exoplayer` with the path to your local copy:
+`/absolute/path/to/exoplayer` with the absolute path to your local copy:
```gradle
-gradle.ext.exoplayerRoot = 'path/to/exoplayer'
+gradle.ext.exoplayerRoot = '/absolute/path/to/exoplayer'
gradle.ext.exoplayerModulePrefix = 'exoplayer-'
apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle')
```
diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 33fbdb7c09..db85258240 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -1,5 +1,179 @@
# Release notes
+### dev-v2 (not yet released)
+
+* Remove deprecated symbols:
+ * Remove `PlaybackPreparer`. UI components that previously had
+ `setPlaybackPreparer` methods will now call `Player.prepare` by default.
+ If this behavior is sufficient, use of `PlaybackPreparer` can be removed
+ from application code without replacement. For custom preparation logic,
+ replace calls to `setPlaybackPreparer` with calls to
+ `setControlDispatcher` on the same components, passing a
+ `ControlDispatcher` that implements custom preparation logic in
+ `dispatchPrepare`. Extend `DefaultControlDispatcher` to avoid having to
+ implement the other `ControlDispatcher` methods.
+ * Remove `setRewindIncrementMs` and `setFastForwardIncrementMs` from UI
+ components. Use `setControlDispatcher` on the same components, passing a
+ `DefaultControlDispatcher` built using `DefaultControlDispatcher(long,
+ long)`.
+ * Remove `PlayerNotificationManager` constructors and `createWith`
+ methods. Use `PlayerNotificationManager.Builder` instead.
+ * Remove `PlayerNotificationManager.setNotificationListener`. Use
+ `PlayerNotificationManager.Builder.setNotificationListener` instead.
+ * Remove `PlayerNotificationManager` `setUseNavigationActions` and
+ `setUseNavigationActionsInCompactView`. Use `setUseNextAction`,
+ `setUsePreviousAction`, `setUseNextActionInCompactView` and
+ `setUsePreviousActionInCompactView` instead.
+ * Remove `Format.create` methods. Use `Format.Builder` instead.
+ * Remove `CastPlayer` specific playlist manipulation methods. Use
+ `setMediaItems`, `addMediaItems`, `removeMediaItem` and `moveMediaItem`
+ instead.
+
+### 2.14.0 (2021-05-13)
+
+* Core Library:
+ * Move `Player` components to `ExoPlayer`. For example
+ `Player.VideoComponent` is now `ExoPlayer.VideoComponent`.
+ * The most used methods of `Player`'s audio, video, text and metadata
+ components have been added directly to `Player`.
+ * Add `Player.getAvailableCommands`, `Player.isCommandAvailable` and
+ `Listener.onAvailableCommandsChanged` to query which commands
+ that can be executed on the player.
+ * Add a `Player.Listener` interface to receive all player events.
+ Component listeners and `EventListener` have been deprecated.
+ * Add `Player.getMediaMetadata`, which returns a combined and structured
+ `MediaMetadata` object. Changes to metadata are reported to
+ `Listener.onMediaMetadataChanged`.
+ * `Player.setPlaybackParameters` no longer accepts null, use
+ `PlaybackParameters.DEFAULT` instead.
+ * Report information about the old and the new playback positions to
+ `Listener.onPositionDiscontinuity`. Add `DISCONTINUITY_REASON_SKIP`
+ and `DISCONTINUITY_REASON_REMOVE` as discontinuity reasons, and rename
+ `DISCONTINUITY_REASON_PERIOD_TRANSITION` to
+ `DISCONTINUITY_REASON_AUTO_TRANSITION`. Remove
+ `DISCONTINUITY_REASON_AD_INSERTION`, for which
+ `DISCONTINUITY_REASON_AUTO_TRANSITION` is used instead
+ ([#6163](https://github.com/google/ExoPlayer/issues/6163),
+ [#4768](https://github.com/google/ExoPlayer/issues/4768)).
+ * Deprecate `ExoPlayer.Builder`. Use `SimpleExoPlayer.Builder` instead.
+ * Move `Player.getRendererCount` and `Player.getRendererType` to
+ `ExoPlayer`.
+ * Use an empty string instead of the URI if the media ID is not explicitly
+ set with `MediaItem.Builder.setMediaId(String)`.
+ * Remove `MediaCodecRenderer.configureCodec()` and add
+ `MediaCodecRenderer.getMediaCodecConfiguration()`. The new method is
+ called just before the `MediaCodec` is created and returns the
+ parameters needed to create and configure the `MediaCodec` instance.
+ Applications can override `MediaCodecRenderer.onCodecInitialized()` to
+ be notified after a `MediaCodec` is initialized, or they can inject a
+ custom `MediaCodecAdapter.Factory` if they want to control how the
+ `MediaCodec` is configured.
+ * Promote `AdaptiveTrackSelection.AdaptationCheckpoint` to `public`
+ visibility to allow Kotlin subclasses of
+ `AdaptiveTrackSelection.Factory`
+ ([#8830](https://github.com/google/ExoPlayer/issues/8830)).
+ * Fix bug when transitions from content to ad periods called
+ `onMediaItemTransition` by mistake.
+ * `AdsLoader.AdViewProvider` and `AdsLoader.OverlayInfo` have been renamed
+ `com.google.android.exoplayer2.ui.AdViewProvider` and
+ `com.google.android.exoplayer2.ui.AdOverlayInfo` respectively.
+ * `CaptionStyleCompat` has been moved to the
+ `com.google.android.exoplayer2.ui` package.
+ * `DebugTextViewHelper` has been moved from the `ui` package to the `util`
+ package.
+* RTSP:
+ * Initial support for RTSP playbacks
+ ([#55](https://github.com/google/ExoPlayer/issues/55)).
+* Downloads and caching:
+ * Fix `CacheWriter` to correctly handle cases where the request `DataSpec`
+ extends beyond the end of the underlying resource. Caching will now
+ succeed in this case, with data up to the end of the resource being
+ cached. This behaviour is enabled by default, and so the
+ `allowShortContent` parameter has been removed
+ ([#7326](https://github.com/google/ExoPlayer/issues/7326)).
+ * Fix `CacheWriter` to correctly handle `DataSource.close` failures, for
+ which it cannot be assumed that data was successfully written to the
+ cache.
+* DRM:
+ * Prepare DRM sessions (and fetch keys) ahead of the playback position
+ ([#4133](https://github.com/google/ExoPlayer/issues/4133)).
+ * Only dispatch DRM session acquire and release events once per period
+ when playing content that uses the same encryption keys for both audio &
+ video tracks. Previously, separate acquire and release events were
+ dispatched for each track in each period.
+ * Include the session state in DRM session-acquired listener methods.
+* UI:
+ * Add `PlayerNotificationManager.Builder`, with the ability to
+ specify which group the notification should belong to.
+ * Remove `setUseSensorRotation` from `PlayerView` and `StyledPlayerView`.
+ Instead, cast the view returned by `getVideoSurfaceView` to
+ `SphericalGLSurfaceView`, and then call `setUseSensorRotation` on the
+ `SphericalGLSurfaceView` directly.
+* Analytics:
+ * Add `onAudioCodecError` and `onVideoCodecError` to `AnalyticsListener`.
+* Video:
+ * Add `Player.getVideoSize()` to retrieve the current size of the video
+ stream. Add `Listener.onVideoSizeChanged(VideoSize)` and deprecate
+ `Listener.onVideoSizeChanged(int, int, int, float)`.
+* Audio:
+ * Report unexpected audio discontinuities to
+ `AnalyticsListener.onAudioSinkError`
+ ([#6384](https://github.com/google/ExoPlayer/issues/6384)).
+ * Allow forcing offload for gapless content even if gapless playback is
+ not supported.
+ * Allow fall back from DTS-HD to DTS when playing via passthrough.
+* Text:
+ * Fix overlapping lines when using `SubtitleView.VIEW_TYPE_WEB`.
+ * Parse SSA/ASS underline & strikethrough info in `Style:` lines
+ ([#8435](https://github.com/google/ExoPlayer/issues/8435)).
+ * Ensure TTML `tts:textAlign` is correctly propagated from `
` nodes to
+ child nodes.
+ * Support TTML `ebutts:multiRowAlign` attributes.
+* Allow the use of Android platform extractors through
+ [MediaParser](https://developer.android.com/reference/android/media/MediaParser).
+ Only supported on API 30+.
+ * You can use platform extractors for progressive media by passing
+ `MediaParserExtractorAdapter.FACTORY` when creating a
+ `ProgressiveMediaSource.Factory`.
+ * You can use platform extractors for HLS by passing
+ `MediaParserHlsMediaChunkExtractor.FACTORY` when creating a
+ `HlsMediaSource.Factory`.
+ * You can use platform extractors for DASH by passing a
+ `DefaultDashChunkSource` that uses `MediaParserChunkExtractor.FACTORY`
+ when creating a `DashMediaSource.Factory`.
+* Cast extension:
+ * Trigger `onMediaItemTransition` event for all reasons except
+ `MEDIA_ITEM_TRANSITION_REASON_REPEAT`.
+* MediaSession extension:
+ * Remove dependency on `exoplayer-core`, relying only `exoplayer-common`
+ instead. To achieve this, `TimelineQueueEditor` uses a new
+ `MediaDescriptionConverter` interface, and no longer relies on
+ `ConcatenatingMediaSource`.
+* Remove deprecated symbols:
+ * Remove `ExoPlayerFactory`. Use `SimpleExoPlayer.Builder` instead.
+ * Remove `Player.DefaultEventListener`. Use `Player.Listener` instead.
+ * Remove `ExtractorMediaSource`. Use `ProgressiveMediaSource` instead.
+ * Remove `DefaultMediaSourceEventListener`. Use `MediaSourceEventListener`
+ instead.
+ * Remove `DashManifest` constructor. Use the remaining constructor with
+ `programInformation` and `serviceDescription` set to `null` instead.
+ * Remove `CryptoInfo.getFrameworkCryptoInfoV16`. Use
+ `CryptoInfo.getFrameworkCryptoInfo` instead.
+ * Remove `NotificationUtil.createNotificationChannel(Context, String, int,
+ int)`. Use `createNotificationChannel(Context, String, int, int, int)`
+ instead.
+ * Remove `PlayerNotificationManager.setNotificationListener`. Use
+ `PlayerNotificationManager.Builder.setNotificationListener` instead.
+ * Remove `PlayerNotificationManager.NotificationListener`
+ `onNotificationStarted(int, Notification)` and
+ `onNotificationCancelled(int)`. Use `onNotificationPosted(int,
+ Notification, boolean)` and `onNotificationCancelled(int, boolean)`
+ instead.
+ * Remove `DownloadNotificationUtil`. Use `DownloadNotificationHelper`
+ instead.
+ * Remove `extension-jobdispatcher` module. Use the `extension-workmanager`
+ module instead.
+
### 2.13.3 (2021-04-14)
* Published via the Google Maven repository (i.e., google()) rather than JCenter.
@@ -7,7 +181,7 @@
* Reset playback speed when live playback speed control becomes unused
([#8664](https://github.com/google/ExoPlayer/issues/8664)).
* Fix playback position issue when re-preparing playback after a
- BehindLiveWindowException
+ `BehindLiveWindowException`
([#8675](https://github.com/google/ExoPlayer/issues/8675)).
* Assume Dolby Vision content is encoded as H264 when calculating maximum
codec input size
@@ -24,6 +198,10 @@
* DASH:
* Parse `forced_subtitle` role from DASH manifests
([#8781](https://github.com/google/ExoPlayer/issues/8781)).
+* DASH:
+ * Fix rounding error that could cause `SegmentTemplate.getSegmentCount()`
+ to return incorrect values
+ ([#8804](https://github.com/google/ExoPlayer/issues/8804)).
* HLS:
* Fix bug of ignoring `EXT-X-START` when setting the live target offset
([#8764](https://github.com/google/ExoPlayer/pull/8764)).
@@ -171,7 +349,7 @@
* 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
+ * 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
diff --git a/constants.gradle b/constants.gradle
index be83f68dad..e3acfd0dc7 100644
--- a/constants.gradle
+++ b/constants.gradle
@@ -13,14 +13,14 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
- releaseVersion = '2.13.3'
- releaseVersionCode = 2013003
+ releaseVersion = '2.14.0'
+ releaseVersionCode = 2014000
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
compileSdkVersion = 30
dexmakerVersion = '2.21.0'
- junitVersion = '4.13-rc-2'
+ junitVersion = '4.13.2'
guavaVersion = '27.1-android'
mockitoVersion = '2.28.2'
mockWebServerVersion = '3.12.0'
@@ -32,9 +32,10 @@ project.ext {
androidxAnnotationVersion = '1.1.0'
androidxAppCompatVersion = '1.1.0'
androidxCollectionVersion = '1.1.0'
+ androidxCoreVersion = '1.3.2'
androidxFuturesVersion = '1.1.0'
androidxMediaVersion = '1.2.1'
- androidxMedia2Version = '1.1.0'
+ androidxMedia2Version = '1.1.2'
androidxMultidexVersion = '2.0.0'
androidxRecyclerViewVersion = '1.1.0'
androidxTestCoreVersion = '1.3.0'
@@ -42,6 +43,7 @@ project.ext {
androidxTestRunnerVersion = '1.3.0'
androidxTestRulesVersion = '1.3.0'
androidxTestServicesStorageVersion = '1.3.0'
+ androidxTestTruthVersion = '1.3.0'
truthVersion = '1.0'
modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
diff --git a/core_settings.gradle b/core_settings.gradle
index 241b94a19b..c0c19abf80 100644
--- a/core_settings.gradle
+++ b/core_settings.gradle
@@ -27,6 +27,7 @@ include modulePrefix + 'library-core'
include modulePrefix + 'library-dash'
include modulePrefix + 'library-extractor'
include modulePrefix + 'library-hls'
+include modulePrefix + 'library-rtsp'
include modulePrefix + 'library-smoothstreaming'
include modulePrefix + 'library-transformer'
include modulePrefix + 'library-ui'
@@ -47,7 +48,6 @@ include modulePrefix + 'extension-opus'
include modulePrefix + 'extension-vp9'
include modulePrefix + 'extension-rtmp'
include modulePrefix + 'extension-leanback'
-include modulePrefix + 'extension-jobdispatcher'
include modulePrefix + 'extension-workmanager'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
@@ -56,6 +56,7 @@ project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/c
project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash')
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
+project(modulePrefix + 'library-rtsp').projectDir = new File(rootDir, 'library/rtsp')
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')
@@ -76,5 +77,4 @@ project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensi
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
-project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')
diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle
index a3c13b382d..8636c71ab0 100644
--- a/demos/cast/build.gradle
+++ b/demos/cast/build.gradle
@@ -55,6 +55,7 @@ dependencies {
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls')
+ implementation project(modulePrefix + 'library-rtsp')
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'extension-cast')
diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml
index d92d9e2303..6bb16ed734 100644
--- a/demos/cast/src/main/AndroidManifest.xml
+++ b/demos/cast/src/main/AndroidManifest.xml
@@ -31,7 +31,8 @@
+ android:theme="@style/Theme.AppCompat"
+ android:exported="true">
diff --git a/demos/gl/build.gradle b/demos/gl/build.gradle
index a2ffa7f41f..01b5808fe7 100644
--- a/demos/gl/build.gradle
+++ b/demos/gl/build.gradle
@@ -46,8 +46,11 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'library-dash')
+ implementation project(modulePrefix + 'library-hls')
+ implementation project(modulePrefix + 'library-rtsp')
+ implementation project(modulePrefix + 'library-smoothstreaming')
+ implementation project(modulePrefix + 'library-ui')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java
index 89bea32581..02399ba86c 100644
--- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java
+++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java
@@ -140,6 +140,7 @@ import javax.microedition.khronos.opengles.GL10;
case "scaleY":
uniform.setFloat(bitmapScaleY);
break;
+ default: // fall out
}
}
for (GlUtil.Attribute copyExternalAttribute : attributes) {
diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java
index 7aee74801f..d1202052fe 100644
--- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java
+++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java
@@ -25,8 +25,8 @@ import android.os.Handler;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.GlUtil;
import com.google.android.exoplayer2.util.TimedValueQueue;
@@ -72,7 +72,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
@Nullable private SurfaceTexture surfaceTexture;
@Nullable private Surface surface;
- @Nullable private Player.VideoComponent videoComponent;
+ @Nullable private ExoPlayer.VideoComponent videoComponent;
/**
* Creates a new instance. Pass {@code true} for {@code requireSecureContext} if the {@link
@@ -151,7 +151,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
*
* @param newVideoComponent The new video component, or {@code null} to detach this view.
*/
- public void setVideoComponent(@Nullable Player.VideoComponent newVideoComponent) {
+ public void setVideoComponent(@Nullable ExoPlayer.VideoComponent newVideoComponent) {
if (newVideoComponent == videoComponent) {
return;
}
diff --git a/demos/main/build.gradle b/demos/main/build.gradle
index d9ec74e2c2..e63fb70673 100644
--- a/demos/main/build.gradle
+++ b/demos/main/build.gradle
@@ -74,6 +74,7 @@ dependencies {
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls')
+ implementation project(modulePrefix + 'library-rtsp')
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'extension-cronet')
diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml
index 053665502b..39a2ec1709 100644
--- a/demos/main/src/main/AndroidManifest.xml
+++ b/demos/main/src/main/AndroidManifest.xml
@@ -41,7 +41,8 @@
+ android:theme="@style/Theme.AppCompat"
+ android:exported="true">
@@ -65,7 +66,8 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTop"
android:label="@string/application_name"
- android:theme="@style/PlayerTheme">
+ android:theme="@style/PlayerTheme"
+ android:exported="true">
diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json
index 24d92bab57..05a36b7fa0 100644
--- a/demos/main/src/main/assets/media.exolist.json
+++ b/demos/main/src/main/assets/media.exolist.json
@@ -513,6 +513,13 @@
"subtitle_mime_type": "text/x-ssa",
"subtitle_language": "en"
},
+ {
+ "name": "SubStation Alpha styling",
+ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
+ "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-styling.ass",
+ "subtitle_mime_type": "text/x-ssa",
+ "subtitle_language": "en"
+ },
{
"name": "MPEG-4 Timed Text",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4"
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 2cf2671aba..027d3846cf 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
@@ -363,7 +363,8 @@ public class DownloadTracker {
private DownloadRequest buildDownloadRequest() {
return downloadHelper
- .getDownloadRequest(Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title)))
+ .getDownloadRequest(
+ Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title.toString())))
.copyWithKeySetId(keySetId);
}
}
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 30fd014504..21dee820c9 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
@@ -51,10 +51,10 @@ import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
-import com.google.android.exoplayer2.ui.DebugTextViewHelper;
import com.google.android.exoplayer2.ui.StyledPlayerControlView;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.DebugTextViewHelper;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.EventLogger;
import com.google.android.exoplayer2.util.Util;
@@ -77,7 +77,7 @@ public class PlayerActivity extends AppCompatActivity
protected StyledPlayerView playerView;
protected LinearLayout debugRootView;
protected TextView debugTextView;
- protected SimpleExoPlayer player;
+ protected @Nullable SimpleExoPlayer player;
private boolean isShowingTrackSelectionDialog;
private Button selectTracksButton;
diff --git a/demos/surface/build.gradle b/demos/surface/build.gradle
index 38de169ae5..9b75d7998b 100644
--- a/demos/surface/build.gradle
+++ b/demos/surface/build.gradle
@@ -46,7 +46,10 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'library-dash')
+ implementation project(modulePrefix + 'library-hls')
+ implementation project(modulePrefix + 'library-rtsp')
+ implementation project(modulePrefix + 'library-smoothstreaming')
+ implementation project(modulePrefix + 'library-ui')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
}
diff --git a/demos/surface/src/main/AndroidManifest.xml b/demos/surface/src/main/AndroidManifest.xml
index c33a9e646b..5fd2890915 100644
--- a/demos/surface/src/main/AndroidManifest.xml
+++ b/demos/surface/src/main/AndroidManifest.xml
@@ -21,7 +21,8 @@
+ android:label="@string/application_name"
+ android:exported="true">
diff --git a/docs/.hgignore b/docs/.hgignore
new file mode 100644
index 0000000000..55ca9f0054
--- /dev/null
+++ b/docs/.hgignore
@@ -0,0 +1,9 @@
+# Mercurial's .hgignore files can only be used in the root directory.
+# You can still apply these rules by adding
+# include:path/to/this/directory/.hgignore to the top-level .hgignore file.
+
+# Ensure same syntax as in .gitignore can be used
+syntax:glob
+
+_site
+Gemfile.lock
diff --git a/docs/CNAME b/docs/CNAME
index ea244c2530..6b8f5dba1c 100644
--- a/docs/CNAME
+++ b/docs/CNAME
@@ -1 +1 @@
-exoplayer.dev
\ No newline at end of file
+exoplayer.dev
diff --git a/docs/_data/navigation.yml b/docs/_data/navigation.yml
index f524eea5f1..612e2fd37f 100644
--- a/docs/_data/navigation.yml
+++ b/docs/_data/navigation.yml
@@ -41,6 +41,8 @@ en:
url: downloading-media.html
- title: Ad insertion
url: ad-insertion.html
+ - title: Retrieving metadata
+ url: retrieving-metadata.html
- title: Live streaming
url: live-streaming.html
- title: Debug logging
@@ -57,6 +59,8 @@ en:
url: smoothstreaming.html
- title: Progressive
url: progressive.html
+ - title: RTSP
+ url: rtsp.html
- title: Advanced topics
children:
- title: Digital rights management
diff --git a/docs/_includes/.DS_Store b/docs/_includes/.DS_Store
deleted file mode 100644
index 83398c199c..0000000000
Binary files a/docs/_includes/.DS_Store and /dev/null differ
diff --git a/docs/_page_fragments/supported-formats-rtsp.md b/docs/_page_fragments/supported-formats-rtsp.md
new file mode 100644
index 0000000000..c87dd690ff
--- /dev/null
+++ b/docs/_page_fragments/supported-formats-rtsp.md
@@ -0,0 +1,11 @@
+ExoPlayer supports both live and on demand RTSP. Supported formats and network
+types are listed below.
+
+**Supported formats**
+* H264
+* AAC (with ADTS bitstream)
+* AC3
+
+**Supported network types**
+* RTP over UDP unicast (multicast is not supported)
+* Interleaved RTSP, RTP over RTSP using TCP
diff --git a/docs/assets/.DS_Store b/docs/assets/.DS_Store
deleted file mode 100644
index 5008ddfcf5..0000000000
Binary files a/docs/assets/.DS_Store and /dev/null differ
diff --git a/docs/customization.md b/docs/customization.md
index 256543d972..7a5cf7e51a 100644
--- a/docs/customization.md
+++ b/docs/customization.md
@@ -136,7 +136,7 @@ just-in-time modifications of the URI, as shown in the following snippet:
DataSource.Factory dataSourceFactory = new ResolvingDataSource.Factory(
httpDataSourceFactory,
// Provide just-in-time URI resolution logic.
- (DataSpec dataSpec) -> dataSpec.withUri(resolveUri(dataSpec.uri)));
+ dataSpec -> dataSpec.withUri(resolveUri(dataSpec.uri)));
~~~
{: .language-java}
diff --git a/docs/dash.md b/docs/dash.md
index f4010bc0c2..8b3ef2571e 100644
--- a/docs/dash.md
+++ b/docs/dash.md
@@ -57,14 +57,14 @@ player.prepare();
You can retrieve the current manifest by calling `Player.getCurrentManifest`.
For DASH you should cast the returned object to `DashManifest`. The
-`onTimelineChanged` callback of `Player.EventListener` is also called whenever
+`onTimelineChanged` callback of `Player.Listener` is also called whenever
the manifest is loaded. This will happen once for a on-demand content, and
possibly many times for live content. The code snippet below shows how an app
can do something whenever the manifest is loaded.
~~~
player.addListener(
- new Player.EventListener() {
+ new Player.Listener() {
@Override
public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) {
diff --git a/docs/doc/reference-v1/allclasses-frame.html b/docs/doc/reference-v1/allclasses-frame.html
deleted file mode 100644
index 73e8c5360a..0000000000
--- a/docs/doc/reference-v1/allclasses-frame.html
+++ /dev/null
@@ -1,311 +0,0 @@
-
-
-
-
-
-All Classes (ExoPlayer library)
-
-
-
-
-
-
Maintains codec event counts, for debugging purposes only.
-
- Counters should be written from the playback thread only. Counters may be read from any thread.
- To ensure that the counter values are correctly reflected between threads, users of this class
- should invoke ensureUpdated() prior to reading and after writing.
Should be invoked from the playback thread after the counters have been updated. Should also
- be invoked from any other thread that wishes to read the counters, before reading. These calls
- ensure that counter updates are made visible to the reading threads.
- Successive calls to this method on a single CryptoInfo will return the same instance.
- Changes to the CryptoInfo will be reflected in the returned object. The return object
- should not be modified directly.
public final class DefaultLoadControl
-extends Object
-implements LoadControl
-
A LoadControl implementation that allows loads to continue in a sequence that prevents
- any loader from getting too far ahead or behind any of the other loaders.
-
- Loads are scheduled so as to fill the available buffer space as rapidly as possible. Once the
- duration of buffered media and the buffer utilization both exceed respective thresholds, the
- control switches to a draining state during which no loads are permitted to start. During
- draining periods, resources such as the device radio have an opportunity to switch into low
- power modes. The control reverts back to the loading state when either the duration of buffered
- media or the buffer utilization fall below respective thresholds.
-
- This implementation of LoadControl integrates with NetworkLock, by registering
- itself as a task with priority NetworkLock.STREAMING_PRIORITY during loading periods,
- and unregistering itself during draining periods.
eventHandler - A handler to use when delivering events to eventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
-
lowWatermarkMs - The minimum duration of media that can be buffered for the control to
- be in the draining state. If less media is buffered, then the control will transition to
- the filling state.
-
highWatermarkMs - The minimum duration of media that can be buffered for the control to
- transition from filling to draining.
-
lowBufferLoad - The minimum fraction of the buffer that must be utilized for the control
- to be in the draining state. If the utilization is lower, then the control will transition
- to the filling state.
-
highBufferLoad - The minimum fraction of the buffer that must be utilized for the control
- to transition from the loading state to the draining state.
bufferSizeContribution - For instances whose Allocator maintains a pool of memory
- for the purpose of satisfying allocation requests, this is a hint indicating the loader's
- desired contribution to the size of the pool, in bytes.
Invoked by a loader to update the control with its current state.
-
- This method must be called by a registered loader whenever its state changes. This is true
- even if the registered loader does not itself wish to start its next load (since the state of
- the loader will still affect whether other registered loaders are allowed to proceed).
- This renderer returns 0 from getTrackCount() in order to request that it should be
- ignored. IllegalStateException is thrown from all other methods documented to indicate
- that they should not be invoked unless the renderer is prepared.
Invoked to make progress when the renderer is in the TrackRenderer.STATE_UNPREPARED state. This
- method will be called repeatedly until true is returned.
-
- This method should return quickly, and should not block if the renderer is currently unable to
- make any useful progress.
Whether the renderer is ready for the ExoPlayer instance to transition to
- ExoPlayer.STATE_ENDED. The player will make this transition as soon as true is
- returned by all of its TrackRenderers.
-
Whether the renderer is able to immediately render media from the current position.
-
- If the renderer is in the TrackRenderer.STATE_STARTED state then returning true indicates that the
- renderer has everything that it needs to continue playback. Returning false indicates that
- the player should pause until the renderer is ready.
-
- If the renderer is in the TrackRenderer.STATE_ENABLED state then returning true indicates that the
- renderer is ready for playback to be started. Returning false indicates that it is not.
-
- If the renderer's state is TrackRenderer.STATE_STARTED, then repeated calls to this method should
- cause the media track to be rendered. If the state is TrackRenderer.STATE_ENABLED, then repeated
- calls should make progress towards getting the renderer into a position where it is ready to
- render the track.
-
- This method should return quickly, and should not block if the renderer is currently unable to
- make any useful progress.
-
True if the cause (i.e. the Throwable returned by Throwable.getCause()) was only caught
- by a fail-safe at the top level of the player. False otherwise.
The default minimum duration of data that must be buffered for playback to resume
- after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
- not due to a user action such as starting playback or seeking).
minBufferMs - A minimum duration of data that must be buffered for playback to start
- or resume following a user action such as a seek.
-
minRebufferMs - A minimum duration of data that must be buffered for playback to resume
- after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
- not due to a user action such as starting playback or seeking).
Invoked when the current value of ExoPlayer.getPlayWhenReady() has been reflected
- by the internal playback thread.
-
- An invocation of this method will shortly follow any call to
- ExoPlayer.setPlayWhenReady(boolean) that changes the state. If multiple calls are
- made in rapid succession, then this method will be invoked only once, after the final state
- has been reflected.
Invoked when an error occurs. The playback state will transition to
- ExoPlayer.STATE_IDLE immediately after this method is invoked. The player instance
- can still be used, and ExoPlayer.release() must still be called on the player should
- it no longer be required.
The implementation is designed to make no assumptions about (and hence impose no restrictions
- on) the type of the media being played, how and where it is stored, or how it is rendered.
- Rather than implementing the loading and rendering of media directly, ExoPlayer instead
- delegates this work to one or more TrackRenderers, which are injected when the player
- is prepared. Hence ExoPlayer is capable of loading and playing any media for which a
- TrackRenderer implementation can be provided.
-
-
MediaCodecAudioTrackRenderer and MediaCodecVideoTrackRenderer can be used for
- the common cases of rendering audio and video. These components in turn require an
- upstreamSampleSource to be injected through their constructors, where upstream
- is defined to denote a component that is closer to the source of the media. This pattern of
- upstream dependency injection is actively encouraged, since it means that the functionality of
- the player is built up through the composition of components that can easily be exchanged for
- alternate implementations. For example a SampleSource implementation may require a
- further upstream data loading component to be injected through its constructor, with different
- implementations enabling the loading of data from various sources.
-
-
-
Threading model
-
-
The figure below shows the ExoPlayer threading model.
-
-
-
-
It is recommended that instances are created and accessed from a single application thread.
- An application's main thread is ideal. Accessing an instance from multiple threads is
- discouraged, however if an application does wish to do this then it may do so provided that it
- ensures accesses are synchronized.
-
An internal playback thread is responsible for managing playback and invoking the
- TrackRenderers in order to load and play the media.
-
TrackRenderer implementations (or any upstream components that they depend on) may
- use additional background threads (e.g. to load data). These are implementation specific.
-
-
-
-
Player state
-
-
The components of an ExoPlayer's state can be divided into two distinct groups. State
- accessed by getSelectedTrack(int) and getPlayWhenReady() is only ever
- changed by invoking the player's methods, and are never changed as a result of operations that
- have been performed asynchronously by the playback thread. In contrast, the playback state
- accessed by getPlaybackState() is only ever changed as a result of operations
- completing on the playback thread, as illustrated below.
-
-
-
The possible playback state transitions are shown below. Transitions can be triggered either
- by changes in the state of the TrackRenderers being used, or as a result of
- prepare(TrackRenderer[]), stop() or release() being invoked.
The player is prepared but not able to immediately play from the current position. The cause
- is TrackRenderer specific, but this state typically occurs when more data needs
- to be buffered for playback to start.
The player is prepared and able to immediately play from the current position. The player will
- be playing if getPlayWhenReady() returns true, and paused otherwise.
Sets whether playback should proceed when getPlaybackState() == STATE_READY.
- If the player is already in this state, then this method can be used to pause and resume
- playback.
-
-
Parameters:
-
playWhenReady - Whether playback should proceed when ready.
Stops playback. Use setPlayWhenReady(false) rather than this method if the intention
- is to pause playback.
-
- Calling this method will cause the playback state to transition to
- STATE_IDLE. The player instance can still be used, and
- release() must still be called on the player if it's no longer required.
-
- Calling this method does not reset the playback position. If this player instance will be used
- to play another video from its start, then seekTo(0) should be called after stopping
- the player and before preparing it for the next video.
Sends a message to a specified component. The message is delivered to the component on the
- playback thread. If the component throws a ExoPlaybackException, then it is
- propagated out of the player as an error.
-
-
Parameters:
-
target - The target to which the message should be delivered.
-
messageType - An integer that can be used to identify the type of the message.
Gets an estimate of the percentage into the media up to which data is buffered.
-
-
Returns:
-
An estimate of the percentage into the media up to which data is buffered. 0 if the
- duration of the media is not known or if no estimate is available.
The version of the library, expressed as an integer.
-
- Three digits are used for each component of VERSION. For example "1.2.3" has the
- corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
- integer version 123045006 (123-045-006).
Extracts samples from a stream using Android's MediaExtractor.
-
- Warning - This class is marked as deprecated because there are known device specific issues
- associated with its use, including playbacks not starting, playbacks stuttering and other
- miscellaneous failures. For mp4, m4a, mp3, webm, mkv, mpeg-ts, ogg, wav and aac playbacks it is
- strongly recommended to use ExtractorSampleSource instead. Where this is not possible
- this class can still be used, but please be aware of the associated risks. Playing container
- formats for which an ExoPlayer extractor does not yet exist (e.g. avi) is a valid use case of
- this class.
-
- Over time we hope to enhance ExtractorSampleSource to support more formats, and hence
- make use of this class unnecessary.
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
-
- Preparation may require reading from the data source (e.g. to determine the available tracks
- and formats). If insufficient data is available then the call will return false
- rather than block. The method can be called repeatedly until the return value indicates
- success.
- Note that whilst the format of a track will remain constant, the format of the actual media
- stream may change dynamically. An example of this is where the track is adaptive
- (i.e. @link MediaFormat.adaptive is true). Hence the track formats returned through
- this method should not be used to configure decoders. Decoder configuration should be
- performed using the formats obtained when reading the media stream through calls to
- SampleSource.SampleSourceReader.readData(int, long, MediaFormatHolder, SampleHolder).
-
- This method should only be called after the source has been prepared.
True if the track has available samples, or if the end of the stream has been
- reached. False if more data needs to be buffered for samples to become available.
Attempts to read a sample or a new format from the source.
-
- This method should only be called when the specified track is enabled.
-
- Note that where multiple tracks are enabled, SampleSource.NOTHING_READ may be returned if the
- next piece of data to be read from the SampleSource corresponds to a different track
- than the one for which data was requested.
-
formatHolder - A MediaFormatHolder object to populate in the case of a new
- format.
-
sampleHolder - A SampleHolder object to populate in the case of a new sample.
- If the caller requires the sample data then it must ensure that SampleHolder.data
- references a valid output buffer.
void register(Object loader,
- int bufferSizeContribution)
-
Registers a loader.
-
-
Parameters:
-
loader - The loader being registered.
-
bufferSizeContribution - For instances whose Allocator maintains a pool of memory
- for the purpose of satisfying allocation requests, this is a hint indicating the loader's
- desired contribution to the size of the pool, in bytes.
boolean update(Object loader,
- long playbackPositionUs,
- long nextLoadPositionUs,
- boolean loading)
-
Invoked by a loader to update the control with its current state.
-
- This method must be called by a registered loader whenever its state changes. This is true
- even if the registered loader does not itself wish to start its next load (since the state of
- the loader will still affect whether other registered loaders are allowed to proceed).
-
-
Parameters:
-
loader - The loader invoking the update.
-
playbackPositionUs - The loader's playback position.
-
nextLoadPositionUs - The loader's next load position. -1 if finished, failed, or if the
- next load position is not yet known.
-
loading - Whether the loader is currently loading data.
-
Returns:
-
True if the loader is allowed to start its next load. False otherwise.
bufferSize - The size of the AudioTrack's buffer, in bytes.
-
bufferSizeMs - The size of the AudioTrack's buffer, in milliseconds, if it is
- configured for PCM output. -1 if it is configured for passthrough output, as the buffered
- media can have a variable bitrate so the duration may be unknown.
-
elapsedSinceLastFeedMs - The time since the AudioTrack was last fed data.
Invoked when the output stream ends, meaning that the last output buffer has been processed
- and the MediaCodec.BUFFER_FLAG_END_OF_STREAM flag has been propagated through the
- decoder.
- Note that when the stream type changes, the AudioTrack must be reinitialized, which can
- introduce a brief gap in audio output. Note also that tracks in the same audio session must
- share the same routing, so a new audio session id will be generated.
source - The upstream source from which the renderer obtains samples.
-
mediaCodecSelector - A decoder selector.
-
drmSessionManager - For use with encrypted content. May be null if support for encrypted
- content is not required.
-
playClearSamplesWithoutKeys - Encrypted media may contain clear (un-encrypted) regions.
- For example a media file may start with a short clear region so as to allow playback to
- begin in parallel with key acquisition. This parameter specifies whether the renderer is
- permitted to play clear regions of encrypted media files before drmSessionManager
- has obtained the keys necessary to decrypt encrypted regions of the media.
source - The upstream source from which the renderer obtains samples.
-
mediaCodecSelector - A decoder selector.
-
drmSessionManager - For use with encrypted content. May be null if support for encrypted
- content is not required.
-
playClearSamplesWithoutKeys - Encrypted media may contain clear (un-encrypted) regions.
- For example a media file may start with a short clear region so as to allow playback to
- begin in parallel with key acquisition. This parameter specifies whether the renderer is
- permitted to play clear regions of encrypted media files before drmSessionManager
- has obtained the keys necessary to decrypt encrypted regions of the media.
-
eventHandler - A handler to use when delivering events to eventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
source - The upstream source from which the renderer obtains samples.
-
mediaCodecSelector - A decoder selector.
-
drmSessionManager - For use with encrypted content. May be null if support for encrypted
- content is not required.
-
playClearSamplesWithoutKeys - Encrypted media may contain clear (un-encrypted) regions.
- For example a media file may start with a short clear region so as to allow playback to
- begin in parallel with key acquisition. This parameter specifies whether the renderer is
- permitted to play clear regions of encrypted media files before drmSessionManager
- has obtained the keys necessary to decrypt encrypted regions of the media.
-
eventHandler - A handler to use when delivering events to eventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
-
audioCapabilities - The audio capabilities for playback on this device. May be null if the
- default capabilities (no encoded audio passthrough support) should be assumed.
-
streamType - The type of audio stream for the AudioTrack.
sources - The upstream sources from which the renderer obtains samples.
-
mediaCodecSelector - A decoder selector.
-
drmSessionManager - For use with encrypted content. May be null if support for encrypted
- content is not required.
-
playClearSamplesWithoutKeys - Encrypted media may contain clear (un-encrypted) regions.
- For example a media file may start with a short clear region so as to allow playback to
- begin in parallel with key acquisition. This parameter specifies whether the renderer is
- permitted to play clear regions of encrypted media files before drmSessionManager
- has obtained the keys necessary to decrypt encrypted regions of the media.
-
eventHandler - A handler to use when delivering events to eventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
-
audioCapabilities - The audio capabilities for playback on this device. May be null if the
- default capabilities (no encoded audio passthrough support) should be assumed.
-
streamType - The type of audio stream for the AudioTrack.
Returns whether encoded audio passthrough should be used for playing back the input format.
- This implementation returns true if the AudioTrack's audio capabilities indicate that
- passthrough is supported.
-
-
Parameters:
-
mimeType - The type of input media.
-
Returns:
-
True if passthrough playback should be used. False otherwise.
If the renderer advances its own playback position then this method returns a corresponding
- MediaClock. If provided, the player will use the returned MediaClock as its
- source of time during playback. A player may have at most one renderer that returns a
- MediaClock from this method.
Invoked when the audio session id becomes known. Once the id is known it will not change
- (and hence this method will not be invoked again) unless the renderer is disabled and then
- subsequently re-enabled.
-
- The default implementation is a no-op. One reason for overriding this method would be to
- instantiate and enable a Virtualizer in order to spatialize the audio channels. For
- this use case, any Virtualizer instances should be released in onDisabled()
- (if not before).
Whether the renderer is ready for the ExoPlayer instance to transition to
- ExoPlayer.STATE_ENDED. The player will make this transition as soon as true is
- returned by all of its TrackRenderers.
-
Whether the renderer is able to immediately render media from the current position.
-
- If the renderer is in the TrackRenderer.STATE_STARTED state then returning true indicates that the
- renderer has everything that it needs to continue playback. Returning false indicates that
- the player should pause until the renderer is ready.
-
- If the renderer is in the TrackRenderer.STATE_ENABLED state then returning true indicates that the
- renderer is ready for playback to be started. Returning false indicates that it is not.
-
Invoked when the output stream ends, meaning that the last output buffer has been processed
- and the MediaCodec.BUFFER_FLAG_END_OF_STREAM flag has been propagated through the
- decoder.
-
Determines whether the existing MediaCodec should be reconfigured for a new format by
- sending codec specific initialization data at the start of the next input buffer.
Invoked when the output stream ends, meaning that the last output buffer has been processed
- and the MediaCodec.BUFFER_FLAG_END_OF_STREAM flag has been propagated through the
- decoder.
protected static final int SOURCE_STATE_READY_READ_MAY_FAIL
-
Value returned by getSourceState() when the source is ready but we might not be able
- to read from it. We transition to this state when an attempt to read a sample fails despite the
- source reporting that samples are available. This can occur when the next sample to be provided
- by the source is for another renderer.
source - The upstream source from which the renderer obtains samples.
-
mediaCodecSelector - A decoder selector.
-
drmSessionManager - For use with encrypted media. May be null if support for encrypted
- media is not required.
-
playClearSamplesWithoutKeys - Encrypted media may contain clear (un-encrypted) regions.
- For example a media file may start with a short clear region so as to allow playback to
- begin in parallel with key acquisition. This parameter specifies whether the renderer is
- permitted to play clear regions of encrypted media files before drmSessionManager
- has obtained the keys necessary to decrypt encrypted regions of the media.
-
eventHandler - A handler to use when delivering events to eventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
sources - The upstream sources from which the renderer obtains samples.
-
mediaCodecSelector - A decoder selector.
-
drmSessionManager - For use with encrypted media. May be null if support for encrypted
- media is not required.
-
playClearSamplesWithoutKeys - Encrypted media may contain clear (un-encrypted) regions.
- For example a media file may start with a short clear region so as to allow playback to
- begin in parallel with key acquisition. This parameter specifies whether the renderer is
- permitted to play clear regions of encrypted media files before drmSessionManager
- has obtained the keys necessary to decrypt encrypted regions of the media.
-
eventHandler - A handler to use when delivering events to eventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
Invoked when the output stream ends, meaning that the last output buffer has been processed
- and the MediaCodec.BUFFER_FLAG_END_OF_STREAM flag has been propagated through the
- decoder.
-
Determines whether the existing MediaCodec should be reconfigured for a new format by
- sending codec specific initialization data at the start of the next input buffer. If true is
- returned then the MediaCodec instance will be reconfigured in this way. If false is
- returned then the instance will be released, and a new instance will be created for the new
- format.
-
Whether the renderer is ready for the ExoPlayer instance to transition to
- ExoPlayer.STATE_ENDED. The player will make this transition as soon as true is
- returned by all of its TrackRenderers.
-
Whether the renderer is able to immediately render media from the current position.
-
- If the renderer is in the TrackRenderer.STATE_STARTED state then returning true indicates that the
- renderer has everything that it needs to continue playback. Returning false indicates that
- the player should pause until the renderer is ready.
-
- If the renderer is in the TrackRenderer.STATE_ENABLED state then returning true indicates that the
- renderer is ready for playback to be started. Returning false indicates that it is not.
-
Invoked to report the number of frames dropped by the renderer. Dropped frames are reported
- whenever the renderer is stopped having dropped frames, and optionally, whenever the count
- reaches a specified threshold whilst the renderer is started.
-
-
Parameters:
-
count - The number of dropped frames.
-
elapsed - The duration in milliseconds over which the frames were dropped. This
- duration is timed from when the renderer was started or from when dropped frames were
- last reported (whichever was more recent), and not from when the first of the reported
- drops occurred.
void onVideoSizeChanged(int width,
- int height,
- int unappliedRotationDegrees,
- float pixelWidthHeightRatio)
-
Invoked each time there's a change in the size of the video being rendered.
-
-
Parameters:
-
width - The video width in pixels.
-
height - The video height in pixels.
-
unappliedRotationDegrees - For videos that require a rotation, this is the clockwise
- rotation in degrees that the application should apply for the video for it to be rendered
- in the correct orientation. This value will always be zero on API levels 21 and above,
- since the renderer will apply all necessary rotations internally. On earlier API levels
- this is not possible. Applications that use TextureView can apply the rotation by
- calling TextureView.setTransform(android.graphics.Matrix). Applications that do not expect to encounter
- rotated videos can safely ignore this parameter.
-
pixelWidthHeightRatio - The width to height ratio of each pixel. For the normal case
- of square pixels this will be equal to 1.0. Different values are indicative of anamorphic
- content.
Determines whether the existing MediaCodec should be reconfigured for a new format by
- sending codec specific initialization data at the start of the next input buffer.
public MediaCodecVideoTrackRenderer(Context context,
- SampleSource source,
- MediaCodecSelector mediaCodecSelector,
- int videoScalingMode,
- long allowedJoiningTimeMs)
-
-
Parameters:
-
context - A context.
-
source - The upstream source from which the renderer obtains samples.
allowedJoiningTimeMs - The maximum duration in milliseconds for which this video renderer
- can attempt to seamlessly join an ongoing playback.
-
drmSessionManager - For use with encrypted content. May be null if support for encrypted
- content is not required.
-
playClearSamplesWithoutKeys - Encrypted media may contain clear (un-encrypted) regions.
- For example a media file may start with a short clear region so as to allow playback to
- begin in parallel with key acquisision. This parameter specifies whether the renderer is
- permitted to play clear regions of encrypted media files before drmSessionManager
- has obtained the keys necessary to decrypt encrypted regions of the media.
-
eventHandler - A handler to use when delivering events to eventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
Whether the renderer is able to immediately render media from the current position.
-
- If the renderer is in the TrackRenderer.STATE_STARTED state then returning true indicates that the
- renderer has everything that it needs to continue playback. Returning false indicates that
- the player should pause until the renderer is ready.
-
- If the renderer is in the TrackRenderer.STATE_ENABLED state then returning true indicates that the
- renderer is ready for playback to be started. Returning false indicates that it is not.
-
Determines whether the existing MediaCodec should be reconfigured for a new format by
- sending codec specific initialization data at the start of the next input buffer. If true is
- returned then the MediaCodec instance will be reconfigured in this way. If false is
- returned then the instance will be released, and a new instance will be created for the new
- format.
-
The duration in microseconds, or C.UNKNOWN_TIME_US if the duration is unknown, or
- C.MATCH_LONGEST_US if the duration should match the duration of the longest track whose
- duration is known.
For formats that belong to an adaptive video track (either describing the track, or describing
- a specific format within it), this is the maximum height of the video in pixels that will be
- encountered in the stream.
For formats that belong to an adaptive video track (either describing the track, or describing
- a specific format within it), this is the maximum width of the video in pixels that will be
- encountered in the stream.
The clockwise rotation that should be applied to the video for it to be rendered in the correct
- orientation, or NO_VALUE if unknown or not applicable.
createAudioFormat(String trackId,
- String mimeType,
- int bitrate,
- int maxInputSize,
- long durationUs,
- int channelCount,
- int sampleRate,
- List<byte[]> initializationData,
- String language,
- int pcmEncoding)
createVideoFormat(String trackId,
- String mimeType,
- int bitrate,
- int maxInputSize,
- long durationUs,
- int width,
- int height,
- List<byte[]> initializationData)
createVideoFormat(String trackId,
- String mimeType,
- int bitrate,
- int maxInputSize,
- long durationUs,
- int width,
- int height,
- List<byte[]> initializationData,
- int rotationDegrees,
- float pixelWidthHeightRatio)
createVideoFormat(String trackId,
- String mimeType,
- int bitrate,
- int maxInputSize,
- long durationUs,
- int width,
- int height,
- List<byte[]> initializationData,
- int rotationDegrees,
- float pixelWidthHeightRatio,
- byte[] projectionData,
- int stereoMode)
The duration in microseconds, or C.UNKNOWN_TIME_US if the duration is unknown, or
- C.MATCH_LONGEST_US if the duration should match the duration of the longest track whose
- duration is known.
For formats that belong to an adaptive video track (either describing the track, or describing
- a specific format within it), this is the maximum width of the video in pixels that will be
- encountered in the stream. Set to NO_VALUE if unknown or not applicable.
For formats that belong to an adaptive video track (either describing the track, or describing
- a specific format within it), this is the maximum height of the video in pixels that will be
- encountered in the stream. Set to NO_VALUE if unknown or not applicable.
The clockwise rotation that should be applied to the video for it to be rendered in the correct
- orientation, or NO_VALUE if unknown or not applicable. Only 0, 90, 180 and 270 are
- supported.
For samples that contain subsamples, this is an offset that should be added to subsample
- timestamps. A value of OFFSET_SAMPLE_RELATIVE indicates that subsample timestamps are
- relative to the timestamps of their parent samples.
public static MediaFormat createVideoFormat(String trackId,
- String mimeType,
- int bitrate,
- int maxInputSize,
- long durationUs,
- int width,
- int height,
- List<byte[]> initializationData)
public static MediaFormat createVideoFormat(String trackId,
- String mimeType,
- int bitrate,
- int maxInputSize,
- long durationUs,
- int width,
- int height,
- List<byte[]> initializationData,
- int rotationDegrees,
- float pixelWidthHeightRatio)
public static MediaFormat createVideoFormat(String trackId,
- String mimeType,
- int bitrate,
- int maxInputSize,
- long durationUs,
- int width,
- int height,
- List<byte[]> initializationData,
- int rotationDegrees,
- float pixelWidthHeightRatio,
- byte[] projectionData,
- int stereoMode)
public static MediaFormat createAudioFormat(String trackId,
- String mimeType,
- int bitrate,
- int maxInputSize,
- long durationUs,
- int channelCount,
- int sampleRate,
- List<byte[]> initializationData,
- String language)
public static MediaFormat createAudioFormat(String trackId,
- String mimeType,
- int bitrate,
- int maxInputSize,
- long durationUs,
- int channelCount,
- int sampleRate,
- List<byte[]> initializationData,
- String language,
- int pcmEncoding)
public static MediaFormat createTextFormat(String trackId,
- String mimeType,
- int bitrate,
- long durationUs,
- String language,
- long subsampleOffsetUs)
Ensures that data is large enough to accommodate a write of a given length at its
- current position.
-
- If the capacity of data is sufficient this method does nothing. If the capacity is
- insufficient then an attempt is made to replace data with a new ByteBuffer
- whose capacity is sufficient. Data up to the current position is copied to the new buffer.
-
-
Parameters:
-
length - The length of the write that must be accommodated, in bytes.
- Preparation may require reading from the data source (e.g. to determine the available tracks
- and formats). If insufficient data is available then the call will return false
- rather than block. The method can be called repeatedly until the return value indicates
- success.
-
-
Parameters:
-
positionUs - The player's current playback position.
- Note that whilst the format of a track will remain constant, the format of the actual media
- stream may change dynamically. An example of this is where the track is adaptive
- (i.e. @link MediaFormat.adaptive is true). Hence the track formats returned through
- this method should not be used to configure decoders. Decoder configuration should be
- performed using the formats obtained when reading the media stream through calls to
- readData(int, long, MediaFormatHolder, SampleHolder).
-
- This method should only be called after the source has been prepared.
boolean continueBuffering(int track,
- long positionUs)
-
Indicates to the source that it should still be buffering data for the specified track.
-
- This method should only be called when the specified track is enabled.
-
-
Parameters:
-
track - The track to continue buffering.
-
positionUs - The current playback position.
-
Returns:
-
True if the track has available samples, or if the end of the stream has been
- reached. False if more data needs to be buffered for samples to become available.
Attempts to read a sample or a new format from the source.
-
- This method should only be called when the specified track is enabled.
-
- Note that where multiple tracks are enabled, SampleSource.NOTHING_READ may be returned if the
- next piece of data to be read from the SampleSource corresponds to a different track
- than the one for which data was requested.
-
formatHolder - A MediaFormatHolder object to populate in the case of a new
- format.
-
sampleHolder - A SampleHolder object to populate in the case of a new sample.
- If the caller requires the sample data then it must ensure that SampleHolder.data
- references a valid output buffer.
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
Neither a sample nor a format was read in full. This may be because insufficient data is
- buffered upstream. If multiple tracks are enabled, this return value may indicate that the
- next piece of data to be returned from the SampleSource corresponds to a different
- track than the one for which data was requested.
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
-
Invoked to make progress when the renderer is in the TrackRenderer.STATE_UNPREPARED state. This
- method will be called repeatedly until true is returned.
-
- This method should return quickly, and should not block if the renderer is currently unable to
- make any useful progress.
- If the renderer's state is TrackRenderer.STATE_STARTED, then repeated calls to this method should
- cause the media track to be rendered. If the state is TrackRenderer.STATE_ENABLED, then repeated
- calls should make progress towards getting the renderer into a position where it is ready to
- render the track.
-
- This method should return quickly, and should not block if the renderer is currently unable to
- make any useful progress.
-
formatHolder - A MediaFormatHolder object to populate in the case of a new format.
-
sampleHolder - A SampleHolder object to populate in the case of a new sample.
- If the caller requires the sample data then it must ensure that SampleHolder.data
- references a valid output buffer.
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
-
- Preparation may require reading from the data source (e.g. to determine the available tracks
- and formats). If insufficient data is available then the call will return false
- rather than block. The method can be called repeatedly until the return value indicates
- success.
- Note that whilst the format of a track will remain constant, the format of the actual media
- stream may change dynamically. An example of this is where the track is adaptive
- (i.e. @link MediaFormat.adaptive is true). Hence the track formats returned through
- this method should not be used to configure decoders. Decoder configuration should be
- performed using the formats obtained when reading the media stream through calls to
- SampleSource.SampleSourceReader.readData(int, long, MediaFormatHolder, SampleHolder).
-
- This method should only be called after the source has been prepared.
True if the track has available samples, or if the end of the stream has been
- reached. False if more data needs to be buffered for samples to become available.
Attempts to read a sample or a new format from the source.
-
- This method should only be called when the specified track is enabled.
-
- Note that where multiple tracks are enabled, SampleSource.NOTHING_READ may be returned if the
- next piece of data to be read from the SampleSource corresponds to a different track
- than the one for which data was requested.
-
formatHolder - A MediaFormatHolder object to populate in the case of a new
- format.
-
sampleHolder - A SampleHolder object to populate in the case of a new sample.
- If the caller requires the sample data then it must ensure that SampleHolder.data
- references a valid output buffer.
Internally, a renderer's lifecycle is managed by the owning ExoPlayer. The player
- will transition its renderers through various states as the overall playback state changes. The
- valid state transitions are shown below, annotated with the methods that are invoked during each
- transition.
-
The renderer has completed necessary preparation. Preparation may include, for example,
- reading the header of a media file to determine the track format and duration.
-
- The renderer should not hold scarce or expensive system resources (e.g. media decoders) and
- should not be actively buffering media data when in this state.
The renderer is enabled. It should either be ready to be started, or be actively working
- towards this state (e.g. a renderer in this state will typically hold any resources that it
- requires, such as media decoders, and will have buffered or be buffering any media data that
- is required to start playback).
If the renderer advances its own playback position then this method returns a corresponding
- MediaClock. If provided, the player will use the returned MediaClock as its
- source of time during playback. A player may have at most one renderer that returns a
- MediaClock from this method.
-
-
Returns:
-
The MediaClock tracking the playback position of the renderer, or null.
Whether the renderer is ready for the ExoPlayer instance to transition to
- ExoPlayer.STATE_ENDED. The player will make this transition as soon as true is
- returned by all of its TrackRenderers.
-
- This method may be called when the renderer is in the following states:
- STATE_ENABLED, STATE_STARTED
-
-
Returns:
-
Whether the renderer is ready for the player to transition to the ended state.
Whether the renderer is able to immediately render media from the current position.
-
- If the renderer is in the STATE_STARTED state then returning true indicates that the
- renderer has everything that it needs to continue playback. Returning false indicates that
- the player should pause until the renderer is ready.
-
- If the renderer is in the STATE_ENABLED state then returning true indicates that the
- renderer is ready for playback to be started. Returning false indicates that it is not.
-
- This method may be called when the renderer is in the following states:
- STATE_ENABLED, STATE_STARTED
-
-
Returns:
-
True if the renderer is ready to render media. False otherwise.
- If the renderer's state is STATE_STARTED, then repeated calls to this method should
- cause the media track to be rendered. If the state is STATE_ENABLED, then repeated
- calls should make progress towards getting the renderer into a position where it is ready to
- render the track.
-
- This method should return quickly, and should not block if the renderer is currently unable to
- make any useful progress.
-
- This method may be called when the renderer is in the following states:
- STATE_ENABLED, STATE_STARTED
-
-
Parameters:
-
positionUs - The current media time in microseconds, measured at the start of the
- current iteration of the rendering loop.
-
elapsedRealtimeUs - SystemClock.elapsedRealtime() in microseconds,
- measured at the start of the current iteration of the rendering loop.
The duration of the track in microseconds, or MATCH_LONGEST_US if
- the track's duration should match that of the longest track whose duration is known, or
- or UNKNOWN_TIME_US if the duration is not known.
Returns an estimate of the absolute position in microseconds up to which data is buffered.
-
- This method may be called when the renderer is in the following states:
- STATE_ENABLED, STATE_STARTED
-
-
Returns:
-
An estimate of the absolute position in microseconds up to which data is buffered,
- or END_OF_TRACK_US if the track is fully buffered, or UNKNOWN_TIME_US if
- no estimate is available.
Gets the current audio capabilities. Note that to be notified when audio capabilities change,
- you can create an instance of AudioCapabilitiesReceiver and register a listener.
-
-
Parameters:
-
context - Context for receiving the initial broadcast.
public final class AudioCapabilitiesReceiver
-extends Object
-
Notifies a listener when the audio playback capabilities change. Call register() to start
- (or resume) receiving notifications, and unregister() to stop.
Registers to notify the listener when audio capabilities change. The current capabilities will
- be returned. It is important to call unregister() so that the listener can be garbage
- collected.
- The underlying framework audio track is created by initialize() and released
- asynchronously by reset() (and configure(java.lang.String, int, int, int), unless the format is unchanged).
- Reinitialization blocks until releasing the old audio track completes. It is safe to
- re-initialize() the instance after calling reset(), without reconfiguration.
-
- Call release() when the instance will no longer be used.
Whether to enable a workaround for an issue where an audio effect does not keep its session
- active across releasing/initializing a new audio track, on platform API version < 21.
public static boolean enablePreV21AudioSessionWorkaround
-
Whether to enable a workaround for an issue where an audio effect does not keep its session
- active across releasing/initializing a new audio track, on platform API version < 21.
-
Attempts to write size bytes from buffer at offset to the audio track.
- Returns a bit field containing RESULT_BUFFER_CONSUMED if the buffer can be released
- (due to having been written), and RESULT_POSITION_DISCONTINUITY if the buffer was
- discontinuous with previously written data.
-
-
Parameters:
-
buffer - The buffer containing audio data to play back.
-
offset - The offset in the buffer from which to consume data.
-
size - The number of bytes to consume from buffer.
-
presentationTimeUs - Presentation timestamp of the next buffer in microseconds.
Sets the stream type for audio track. If the stream type has changed, isInitialized()
- will return false and the caller must re-initialize(int) the audio track
- before writing more data. The caller must not reuse the audio session identifier when
- re-initializing with a new stream type.
-
-
Parameters:
-
streamType - The stream type to use for audio output.
Releases the underlying audio track asynchronously. Calling initialize() will block
- until the audio track has been released, so it is safe to initialize immediately after
- resetting. The audio session may remain active until the instance is release()d.
Invoked when the current upstream load operation is canceled.
-
-
-
-
void
-
onLoadCompleted(int sourceId,
- long bytesLoaded,
- int type,
- int trigger,
- Format format,
- long mediaStartTimeMs,
- long mediaEndTimeMs,
- long elapsedRealtimeMs,
- long loadDurationMs)
-
Invoked when the current load operation completes.
void onLoadCompleted(int sourceId,
- long bytesLoaded,
- int type,
- int trigger,
- Format format,
- long mediaStartTimeMs,
- long mediaEndTimeMs,
- long elapsedRealtimeMs,
- long loadDurationMs)
-
Invoked when the current load operation completes.
BaseMediaChunk(DataSource dataSource,
- DataSpec dataSpec,
- int trigger,
- Format format,
- long startTimeUs,
- long endTimeUs,
- int chunkIndex,
- boolean isMediaFormatFinal,
- int parentId)
Whether getMediaFormat() and getDrmInitData() can be called at any time to
- obtain the chunk's media format and drm initialization data. If false, these methods are only
- guaranteed to return correct data after the first sample data has been output from the chunk.
public BaseMediaChunk(DataSource dataSource,
- DataSpec dataSpec,
- int trigger,
- Format format,
- long startTimeUs,
- long endTimeUs,
- int chunkIndex,
- boolean isMediaFormatFinal,
- int parentId)
trigger - The reason for this chunk being selected.
-
format - The format of the stream to which this chunk belongs.
-
startTimeUs - The start time of the media contained by the chunk, in microseconds.
-
endTimeUs - The end time of the media contained by the chunk, in microseconds.
-
chunkIndex - The index of the chunk.
-
isMediaFormatFinal - True if getMediaFormat() and getDrmInitData() can
- be called at any time to obtain the media format and drm initialization data. False if
- these methods are only guaranteed to return correct data after the first sample data has
- been output from the chunk.
-
parentId - Identifier for a parent from which this chunk originates.
public Chunk(DataSource dataSource,
- DataSpec dataSpec,
- int type,
- int trigger,
- Format format,
- int parentId)
-
-
Parameters:
-
dataSource - The source from which the data should be loaded.
-
dataSpec - Defines the data to be loaded. dataSpec.length must not exceed
- Integer.MAX_VALUE. If dataSpec.length == C.LENGTH_UNBOUNDED then
- the length resolved by dataSource.open(dataSpec) must not exceed
- Integer.MAX_VALUE.
input - An ExtractorInput from which to read the sample data.
-
length - The maximum length to read from the input.
-
allowEndOfInput - True if encountering the end of the input having read no data is
- allowed, and should result in C.RESULT_END_OF_INPUT being returned. False if it
- should be considered an error, causing an EOFException to be thrown.
-
Returns:
-
The number of bytes appended.
-
Throws:
-
IOException - If an error occurred reading from the input.
public final class ChunkOperationHolder
-extends Object
-
Holds a chunk operation, which consists of a either:
-
-
The number of MediaChunks that should be retained on the queue (queueSize)
- together with the next Chunk to load (chunk). chunk may be null if the
- next chunk cannot be provided yet.
-
A flag indicating that the end of the stream has been reached (endOfStream).
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
-
- Preparation may require reading from the data source (e.g. to determine the available tracks
- and formats). If insufficient data is available then the call will return false
- rather than block. The method can be called repeatedly until the return value indicates
- success.
- Note that whilst the format of a track will remain constant, the format of the actual media
- stream may change dynamically. An example of this is where the track is adaptive
- (i.e. @link MediaFormat.adaptive is true). Hence the track formats returned through
- this method should not be used to configure decoders. Decoder configuration should be
- performed using the formats obtained when reading the media stream through calls to
- SampleSource.SampleSourceReader.readData(int, long, MediaFormatHolder, SampleHolder).
-
- This method should only be called after the source has been prepared.
True if the track has available samples, or if the end of the stream has been
- reached. False if more data needs to be buffered for samples to become available.
Attempts to read a sample or a new format from the source.
-
- This method should only be called when the specified track is enabled.
-
- Note that where multiple tracks are enabled, SampleSource.NOTHING_READ may be returned if the
- next piece of data to be read from the SampleSource corresponds to a different track
- than the one for which data was requested.
-
formatHolder - A MediaFormatHolder object to populate in the case of a new
- format.
-
sampleHolder - A SampleHolder object to populate in the case of a new sample.
- If the caller requires the sample data then it must ensure that SampleHolder.data
- references a valid output buffer.
- This method should only be called when the source is enabled.
-
-
Parameters:
-
queue - A representation of the currently buffered MediaChunks.
-
playbackPositionUs - The current playback position. If the queue is empty then this
- parameter is the position from which playback is expected to start (or restart) and hence
- should be interpreted as a seek position.
-
out - A holder for the next operation, whose ChunkOperationHolder.endOfStream is
- initially set to false, whose ChunkOperationHolder.queueSize is initially equal to
- the length of the queue, and whose ChunkOperationHolder.chunk is initially equal to
- null or a Chunk previously supplied by the ChunkSource that the caller has
- not yet finished loading. In the latter case the chunk can either be replaced or left
- unchanged. Note that leaving the chunk unchanged is both preferred and more efficient than
- replacing it with a new but identical chunk.
ContainerMediaChunk(DataSource dataSource,
- DataSpec dataSpec,
- int trigger,
- Format format,
- long startTimeUs,
- long endTimeUs,
- int chunkIndex,
- long sampleOffsetUs,
- ChunkExtractorWrapper extractorWrapper,
- MediaFormat mediaFormat,
- int adaptiveMaxWidth,
- int adaptiveMaxHeight,
- DrmInitData drmInitData,
- boolean isMediaFormatFinal,
- int parentId)
public ContainerMediaChunk(DataSource dataSource,
- DataSpec dataSpec,
- int trigger,
- Format format,
- long startTimeUs,
- long endTimeUs,
- int chunkIndex,
- long sampleOffsetUs,
- ChunkExtractorWrapper extractorWrapper,
- MediaFormat mediaFormat,
- int adaptiveMaxWidth,
- int adaptiveMaxHeight,
- DrmInitData drmInitData,
- boolean isMediaFormatFinal,
- int parentId)
trigger - The reason for this chunk being selected.
-
format - The format of the stream to which this chunk belongs.
-
startTimeUs - The start time of the media contained by the chunk, in microseconds.
-
endTimeUs - The end time of the media contained by the chunk, in microseconds.
-
chunkIndex - The index of the chunk.
-
sampleOffsetUs - An offset to add to the sample timestamps parsed by the extractor.
-
extractorWrapper - A wrapped extractor to use for parsing the data.
-
mediaFormat - The MediaFormat of the chunk, if known. May be null if the data is
- known to define its own format.
-
adaptiveMaxWidth - If this chunk contains video and is part of an adaptive playback, this
- is the maximum width of the video in pixels that will be encountered during the playback.
- MediaFormat.NO_VALUE otherwise.
-
adaptiveMaxHeight - If this chunk contains video and is part of an adaptive playback, this
- is the maximum height of the video in pixels that will be encountered during the playback.
- MediaFormat.NO_VALUE otherwise.
-
drmInitData - The DrmInitData for the chunk. Null if the media is not drm
- protected. May also be null if the data is known to define its own initialization data.
-
isMediaFormatFinal - True if mediaFormat and drmInitData are known to be
- correct and final. False if the data may define its own format or initialization data.
-
parentId - Identifier for a parent from which this chunk originates.
input - An ExtractorInput from which to read the sample data.
-
length - The maximum length to read from the input.
-
allowEndOfInput - True if encountering the end of the input having read no data is
- allowed, and should result in C.RESULT_END_OF_INPUT being returned. False if it
- should be considered an error, causing an EOFException to be thrown.
-
Returns:
-
The number of bytes appended.
-
Throws:
-
IOException - If an error occurred reading from the input.
public DataChunk(DataSource dataSource,
- DataSpec dataSpec,
- int type,
- int trigger,
- Format format,
- int parentId,
- byte[] data)
-
-
Parameters:
-
dataSource - The source from which the data should be loaded.
-
dataSpec - Defines the data to be loaded. dataSpec.length must not exceed
- Integer.MAX_VALUE. If dataSpec.length == C.LENGTH_UNBOUNDED then
- the length resolved by dataSource.open(dataSpec) must not exceed
- Integer.MAX_VALUE.
public Format(String id,
- String mimeType,
- int width,
- int height,
- float frameRate,
- int numChannels,
- int audioSamplingRate,
- int bitrate,
- String language)
-
-
Parameters:
-
id - The format identifier.
-
mimeType - The format mime type.
-
width - The width of the video in pixels, or -1 if unknown or not applicable.
-
height - The height of the video in pixels, or -1 if unknown or not applicable.
-
frameRate - The frame rate of the video in frames per second, or -1 if unknown or not
- applicable.
-
numChannels - The number of audio channels, or -1 if unknown or not applicable.
-
audioSamplingRate - The audio sampling rate in Hz, or -1 if unknown or not applicable.
-
bitrate - The average bandwidth of the format in bits per second.
public Format(String id,
- String mimeType,
- int width,
- int height,
- float frameRate,
- int audioChannels,
- int audioSamplingRate,
- int bitrate,
- String language,
- String codecs)
-
-
Parameters:
-
id - The format identifier.
-
mimeType - The format mime type.
-
width - The width of the video in pixels, or -1 if unknown or not applicable.
-
height - The height of the video in pixels, or -1 if unknown or not applicable.
-
frameRate - The frame rate of the video in frames per second, or -1 if unknown or not
- applicable.
-
audioChannels - The number of audio channels, or -1 if unknown or not applicable.
-
audioSamplingRate - The audio sampling rate in Hz, or -1 if unknown or not applicable.
-
bitrate - The average bandwidth of the format in bits per second.
public static final class FormatEvaluator.AdaptiveEvaluator
-extends Object
-implements FormatEvaluator
-
An adaptive evaluator for video formats, which attempts to select the best quality possible
- given the current network conditions and state of the buffer.
-
- This implementation should be used for video only, and should not be used for audio. It is a
- reference implementation only. It is recommended that application developers implement their
- own adaptive evaluator to more precisely suit their use case.
AdaptiveEvaluator(BandwidthMeter bandwidthMeter,
- int maxInitialBitrate,
- int minDurationForQualityIncreaseMs,
- int maxDurationForQualityDecreaseMs,
- int minDurationToRetainAfterDiscardMs,
- float bandwidthFraction)
public AdaptiveEvaluator(BandwidthMeter bandwidthMeter,
- int maxInitialBitrate,
- int minDurationForQualityIncreaseMs,
- int maxDurationForQualityDecreaseMs,
- int minDurationToRetainAfterDiscardMs,
- float bandwidthFraction)
-
-
Parameters:
-
bandwidthMeter - Provides an estimate of the currently available bandwidth.
-
maxInitialBitrate - The maximum bitrate in bits per second that should be assumed
- when bandwidthMeter cannot provide an estimate due to playback having only just started.
-
minDurationForQualityIncreaseMs - The minimum duration of buffered data required for
- the evaluator to consider switching to a higher quality format.
-
maxDurationForQualityDecreaseMs - The maximum duration of buffered data required for
- the evaluator to consider switching to a lower quality format.
-
minDurationToRetainAfterDiscardMs - When switching to a significantly higher quality
- format, the evaluator may discard some of the media that it has already buffered at the
- lower quality, so as to switch up to the higher quality faster. This is the minimum
- duration of media that must be retained at the lower quality.
-
bandwidthFraction - The fraction of the available bandwidth that the evaluator should
- consider available for use. Setting to a value less than 1 is recommended to account
- for inaccuracies in the bandwidth estimator.
- When the method is invoked, evaluation will contain the currently selected
- format (null for the first evaluation), the most recent trigger (TRIGGER_INITIAL for the
- first evaluation) and the current queue size. The implementation should update these
- fields as necessary.
-
- The trigger should be considered "sticky" for as long as a given representation is selected,
- and so should only be changed if the representation is also changed.
- When the method is invoked, evaluation will contain the currently selected
- format (null for the first evaluation), the most recent trigger (TRIGGER_INITIAL for the
- first evaluation) and the current queue size. The implementation should update these
- fields as necessary.
-
- The trigger should be considered "sticky" for as long as a given representation is selected,
- and so should only be changed if the representation is also changed.
- When the method is invoked, evaluation will contain the currently selected
- format (null for the first evaluation), the most recent trigger (TRIGGER_INITIAL for the
- first evaluation) and the current queue size. The implementation should update these
- fields as necessary.
-
- The trigger should be considered "sticky" for as long as a given representation is selected,
- and so should only be changed if the representation is also changed.
An adaptive evaluator for video formats, which attempts to select the best quality possible
- given the current network conditions and state of the buffer.
- When the method is invoked, evaluation will contain the currently selected
- format (null for the first evaluation), the most recent trigger (TRIGGER_INITIAL for the
- first evaluation) and the current queue size. The implementation should update these
- fields as necessary.
-
- The trigger should be considered "sticky" for as long as a given representation is selected,
- and so should only be changed if the representation is also changed.
-
-
Parameters:
-
queue - A read only representation of the currently buffered MediaChunks.
-
playbackPositionUs - The current playback position.
-
formats - The formats from which to select, ordered by decreasing bandwidth.
input - An ExtractorInput from which to read the sample data.
-
length - The maximum length to read from the input.
-
allowEndOfInput - True if encountering the end of the input having read no data is
- allowed, and should result in C.RESULT_END_OF_INPUT being returned. False if it
- should be considered an error, causing an EOFException to be thrown.
-
Returns:
-
The number of bytes appended.
-
Throws:
-
IOException - If an error occurred reading from the input.
MediaChunk(DataSource dataSource,
- DataSpec dataSpec,
- int trigger,
- Format format,
- long startTimeUs,
- long endTimeUs,
- int chunkIndex)
-
-
-
MediaChunk(DataSource dataSource,
- DataSpec dataSpec,
- int trigger,
- Format format,
- long startTimeUs,
- long endTimeUs,
- int chunkIndex,
- int parentId)
public MediaChunk(DataSource dataSource,
- DataSpec dataSpec,
- int trigger,
- Format format,
- long startTimeUs,
- long endTimeUs,
- int chunkIndex,
- int parentId)
public SingleSampleMediaChunk(DataSource dataSource,
- DataSpec dataSpec,
- int trigger,
- Format format,
- long startTimeUs,
- long endTimeUs,
- int chunkIndex,
- MediaFormat sampleFormat,
- DrmInitData sampleDrmInitData,
- int parentId)
public static int[] selectVideoFormats(List<? extends FormatWrapper> formatWrappers,
- String[] allowedContainerMimeTypes,
- boolean filterHdFormats,
- boolean orientationMayChange,
- boolean secureDecoder,
- int viewportWidth,
- int viewportHeight)
- throws MediaCodecUtil.DecoderQueryException
-
Chooses a suitable subset from a number of video formats.
-
- A format is filtered (i.e. not selected) if:
-
-
allowedContainerMimeTypes is non-null and the format does not have one of the
- permitted mime types.
-
filterHdFormats is true and the format is HD.
-
It's determined that the video decoder isn't powerful enough to decode the format.
-
There exists another format of lower resolution whose resolution exceeds the maximum size
- in pixels that the video can be rendered within the viewport.
-
-
-
Parameters:
-
formatWrappers - Wrapped formats from which to select.
-
allowedContainerMimeTypes - An array of allowed container mime types. Null allows all
- mime types.
-
filterHdFormats - True to filter HD formats. False otherwise.
-
orientationMayChange - True if the video's orientation may change with respect to the
- viewport during playback.
-
secureDecoder - True if secure decoder is required.
-
viewportWidth - The width in pixels of the viewport within which the video will be
- displayed. If the viewport size may change, this should be set to the maximum possible
- width. -1 if selection should not be constrained by a viewport.
-
viewportHeight - The height in pixels of the viewport within which the video will be
- displayed. If the viewport size may change, this should be set to the maximum possible
- height. -1 if selection should not be constrained by a viewport.
-
Returns:
-
An array holding the indices of the selected formats.
An adaptive evaluator for video formats, which attempts to select the best quality possible
- given the current network conditions and state of the buffer.
- May also be used for fixed duration content, in which case the call is equivalent to calling
- the other constructor, passing manifestFetcher.getManifest() is the first argument.
-
-
Parameters:
-
manifestFetcher - A fetcher for the manifest, which must have already successfully
- completed an initial load.
-
trackSelector - Selects tracks from manifest periods to be exposed by this source.
-
dataSource - A DataSource suitable for loading the media data.
-
adaptiveFormatEvaluator - For adaptive tracks, selects from the available formats.
-
liveEdgeLatencyMs - For live streams, the number of milliseconds that the playback should
- lag behind the "live edge" (i.e. the end of the most recently defined media in the
- manifest). Choosing a small value will minimize latency introduced by the player, however
- note that the value sets an upper bound on the length of media that the player can buffer.
- Hence a small value may increase the probability of rebuffering and playback failures.
-
elapsedRealtimeOffsetMs - If known, an estimate of the instantaneous difference between
- server-side unix time and SystemClock.elapsedRealtime() in milliseconds, specified
- as the server's unix time minus the local elapsed time. It unknown, set to 0.
-
eventHandler - A handler to use when delivering events to EventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
-
eventSourceId - An identifier that gets passed to eventListener methods.
manifestFetcher - A fetcher for the manifest, which must have already successfully
- completed an initial load.
-
trackSelector - Selects tracks from manifest periods to be exposed by this source.
-
dataSource - A DataSource suitable for loading the media data.
-
adaptiveFormatEvaluator - For adaptive tracks, selects from the available formats.
-
liveEdgeLatencyMs - For live streams, the number of milliseconds that the playback should
- lag behind the "live edge" (i.e. the end of the most recently defined media in the
- manifest). Choosing a small value will minimize latency introduced by the player, however
- note that the value sets an upper bound on the length of media that the player can buffer.
- Hence a small value may increase the probability of rebuffering and playback failures.
-
elapsedRealtimeOffsetMs - If known, an estimate of the instantaneous difference between
- server-side unix time and SystemClock.elapsedRealtime() in milliseconds, specified
- as the server's unix time minus the local elapsed time. It unknown, set to 0.
-
startAtLiveEdge - True if the stream should start at the live edge; false if it should
- at the beginning of the live window.
-
eventHandler - A handler to use when delivering events to EventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
-
eventSourceId - An identifier that gets passed to eventListener methods.
queue - A representation of the currently buffered MediaChunks.
-
playbackPositionUs - The current playback position. If the queue is empty then this
- parameter is the position from which playback is expected to start (or restart) and hence
- should be interpreted as a seek position.
-
out - A holder for the next operation, whose ChunkOperationHolder.endOfStream is
- initially set to false, whose ChunkOperationHolder.queueSize is initially equal to
- the length of the queue, and whose ChunkOperationHolder.chunk is initially equal to
- null or a Chunk previously supplied by the ChunkSource that the caller has
- not yet finished loading. In the latter case the chunk can either be replaced or left
- unchanged. Note that leaving the chunk unchanged is both preferred and more efficient than
- replacing it with a new but identical chunk.
int getSegmentNum(long timeUs,
- long periodDurationUs)
-
Returns the segment number of the segment containing a given media time.
-
- If the given media time is outside the range of the index, then the returned segment number is
- clamped to getFirstSegmentNum() (if the given media time is earlier the start of the
- first segment) or getLastSegmentNum(long) (if the given media time is later then the
- end of the last segment).
-
-
Parameters:
-
timeUs - The time in microseconds.
-
periodDurationUs - The duration of the enclosing period in microseconds, or
- C.UNKNOWN_TIME_US if the period's duration is not yet known.
Returns the segment number of the last segment, or INDEX_UNBOUNDED.
-
- An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a
- SegmentTimeline element, and if the period duration is not yet known. In this case the caller
- must manually determine the window of currently available segments.
-
-
Parameters:
-
periodDurationUs - The duration of the enclosing period in microseconds, or
- C.UNKNOWN_TIME_US if the period's duration is not yet known.
Returns true if segments are defined explicitly by the index.
-
- If true is returned, each segment is defined explicitly by the index data, and all of the
- listed segments are guaranteed to be available at the time when the index was obtained.
-
- If false is returned then segment information was derived from properties such as a fixed
- segment duration. If the presentation is dynamic, it's possible that only a subset of the
- segments are available.
-
-
Returns:
-
True if segments are defined explicitly by the index. False otherwise.
context - A context. May be null if filterVideoRepresentations == false.
-
filterVideoRepresentations - Whether video representations should be filtered according to
- the capabilities of the device. It is strongly recommended to set this to true,
- unless the application has already verified that all representations are playable.
-
filterProtectedHdContent - Whether video representations that are both drm protected and
- high definition should be filtered when tracks are built. If
- filterVideoRepresentations == false then this parameter is ignored.
- If child Representation elements contain ContentProtection elements, then it is required that
- they all define the same ones. If they do, the ContentProtection elements are bubbled up to the
- AdaptationSet. Child Representation elements defining different ContentProtection elements is
- considered an error.
protected SegmentBase.SingleSegmentBase buildSingleSegmentBase(RangedUri initialization,
- long timescale,
- long presentationTimeOffset,
- long indexStart,
- long indexLength)
Attempts to merge this RangedUri with another and an optional common base uri.
-
- A merge is successful if both instances define the same Uri after resolution with the
- base Uri, and if one starts the byte after the other ends, forming a contiguous region with
- no overlap.
-
- If other is null then the merge is considered unsuccessful, and null is returned.
Returns the segment number of the segment containing a given media time.
-
- If the given media time is outside the range of the index, then the returned segment number is
- clamped to DashSegmentIndex.getFirstSegmentNum() (if the given media time is earlier the start of the
- first segment) or DashSegmentIndex.getLastSegmentNum(long) (if the given media time is later then the
- end of the last segment).
- An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a
- SegmentTimeline element, and if the period duration is not yet known. In this case the caller
- must manually determine the window of currently available segments.
Returns true if segments are defined explicitly by the index.
-
- If true is returned, each segment is defined explicitly by the index data, and all of the
- listed segments are guaranteed to be available at the time when the index was obtained.
-
- If false is returned then segment information was derived from properties such as a fixed
- segment duration. If the presentation is dynamic, it's possible that only a subset of the
- segments are available.
newInstance(String contentId,
- long revisionId,
- Format format,
- String uri,
- long initializationStart,
- long initializationEnd,
- long indexStart,
- long indexEnd,
- String customCacheKey,
- long contentLength)
public static Representation.SingleSegmentRepresentation newInstance(String contentId,
- long revisionId,
- Format format,
- String uri,
- long initializationStart,
- long initializationEnd,
- long indexStart,
- long indexEnd,
- String customCacheKey,
- long contentLength)
-
-
Parameters:
-
contentId - Identifies the piece of content to which this representation belongs.
-
revisionId - Identifies the revision of the content.
-
format - The format of the representation.
-
uri - The uri of the media.
-
initializationStart - The offset of the first byte of initialization data.
-
initializationEnd - The offset of the last byte of initialization data.
-
indexStart - The offset of the first byte of index data.
-
indexEnd - The offset of the last byte of index data.
- If the media for a given (contentId can change over time without a change to the
- format's Format.id (e.g. as a result of re-encoding the media with an
- updated encoder), then this identifier must uniquely identify the revision of the media. The
- timestamp at which the media was encoded is often a suitable.
public MultiSegmentBase(RangedUri initialization,
- long timescale,
- long presentationTimeOffset,
- int startNumber,
- long duration,
- List<SegmentBase.SegmentTimelineElement> segmentTimeline)
-
-
Parameters:
-
initialization - A RangedUri corresponding to initialization data, if such data
- exists.
-
timescale - The timescale in units per second.
-
presentationTimeOffset - The presentation time offset. The value in seconds is the
- division of this value and timescale.
-
startNumber - The sequence number of the first segment.
-
duration - The duration of each segment in the case of fixed duration segments. The
- value in seconds is the division of this value and timescale. If
- segmentTimeline is non-null then this parameter is ignored.
-
segmentTimeline - A segment timeline corresponding to the segments. If null, then
- segments are assumed to be of fixed duration as specified by the duration
- parameter.
initialization - A RangedUri corresponding to initialization data, if such data
- exists.
-
timescale - The timescale in units per second.
-
presentationTimeOffset - The presentation time offset. The value in seconds is the
- division of this value and timescale.
-
startNumber - The sequence number of the first segment.
-
duration - The duration of each segment in the case of fixed duration segments. The
- value in seconds is the division of this value and timescale. If
- segmentTimeline is non-null then this parameter is ignored.
-
segmentTimeline - A segment timeline corresponding to the segments. If null, then
- segments are assumed to be of fixed duration as specified by the duration
- parameter.
-
mediaSegments - A list of RangedUris indicating the locations of the segments.
initialization - A RangedUri corresponding to initialization data, if such data
- exists. The value of this parameter is ignored if initializationTemplate is
- non-null.
-
timescale - The timescale in units per second.
-
presentationTimeOffset - The presentation time offset. The value in seconds is the
- division of this value and timescale.
-
startNumber - The sequence number of the first segment.
-
duration - The duration of each segment in the case of fixed duration segments. The
- value in seconds is the division of this value and timescale. If
- segmentTimeline is non-null then this parameter is ignored.
-
segmentTimeline - A segment timeline corresponding to the segments. If null, then
- segments are assumed to be of fixed duration as specified by the duration
- parameter.
-
initializationTemplate - A template defining the location of initialization data, if
- such data exists. If non-null then the initialization parameter is ignored. If
- null then initialization will be used.
-
mediaTemplate - A template defining the location of each media segment.
void onTimestampResolved(UtcTimingElement utcTiming,
- long elapsedRealtimeOffset)
-
Invoked when the element has been resolved.
-
-
Parameters:
-
utcTiming - The element that was resolved.
-
elapsedRealtimeOffset - The offset between the resolved UTC time and
- SystemClock.elapsedRealtime() in milliseconds, specified as the UTC time minus
- the local elapsed time.
uriDataSource - A source to use should loading from a URI be necessary.
-
timingElement - The element to resolve.
-
timingElementElapsedRealtime - The SystemClock.elapsedRealtime() timestamp at
- which the element was obtained. Used if the element contains a timestamp directly.
-
callback - The callback to invoke on resolution or failure.
Whether the session requires a secure decoder for the specified mime type.
-
- Normally this method should return MediaCrypto.requiresSecureDecoderComponent(String),
- however in some cases implementations may wish to modify the return value (i.e. to force a
- secure decoder even when one is not required).
-
Whether the session requires a secure decoder for the specified mime type.
-
- Normally this method should return MediaCrypto.requiresSecureDecoderComponent(String),
- however in some cases implementations may wish to modify the return value (i.e. to force a
- secure decoder even when one is not required).
-
public static final int REASON_INSTANTIATION_ERROR
-
There device advertises support for the requested DRM scheme, but there was an error
- instantiating it. The cause can be retrieved using Throwable.getCause().
- If seeking is not supported then the only valid seek position is the start of the file, and so
- SeekMap.getPosition(long) will return 0 for all input values.
- If the end of the input is found having read no data, then behavior is dependent on
- allowEndOfInput. If allowEndOfInput == true then false is returned.
- Otherwise an EOFException is thrown.
-
- Encountering the end of input having partially satisfied the read is always considered an
- error, and will result in an EOFException being thrown.
target - A target array into which data should be written.
-
offset - The offset into the target array at which to write.
-
length - The number of bytes to read from the input.
-
allowEndOfInput - True if encountering the end of the input having read no data is
- allowed, and should result in false being returned. False if it should be
- considered an error, causing an EOFException to be thrown.
-
Returns:
-
True if the read was successful. False if the end of the input was encountered having
- read no data.
-
Throws:
-
EOFException - If the end of input was encountered having partially satisfied the read
- (i.e. having read at least one byte, but fewer than length), or if no bytes were
- read and allowEndOfInput is false.
-
IOException - If an error occurs reading from the input.
length - The number of bytes to skip from the input.
-
allowEndOfInput - True if encountering the end of the input having skipped no data is
- allowed, and should result in false being returned. False if it should be
- considered an error, causing an EOFException to be thrown.
-
Returns:
-
True if the skip was successful. False if the end of the input was encountered having
- skipped no data.
-
Throws:
-
EOFException - If the end of input was encountered having partially satisfied the skip
- (i.e. having skipped at least one byte, but fewer than length), or if no bytes were
- skipped and allowEndOfInput is false.
-
IOException - If an error occurs reading from the input.
Peeks length bytes from the peek position, writing them into target at index
- offset. The current read position is left unchanged.
-
- If the end of the input is found having peeked no data, then behavior is dependent on
- allowEndOfInput. If allowEndOfInput == true then false is returned.
- Otherwise an EOFException is thrown.
-
- Calling ExtractorInput.resetPeekPosition() resets the peek position to equal the current read
- position, so the caller can peek the same data again. Reading and skipping also reset the peek
- position.
target - A target array into which data should be written.
-
offset - The offset into the target array at which to write.
-
length - The number of bytes to peek from the input.
-
allowEndOfInput - True if encountering the end of the input having peeked no data is
- allowed, and should result in false being returned. False if it should be
- considered an error, causing an EOFException to be thrown.
-
Returns:
-
True if the peek was successful. False if the end of the input was encountered having
- peeked no data.
-
Throws:
-
EOFException - If the end of input was encountered having partially satisfied the peek
- (i.e. having peeked at least one byte, but fewer than length), or if no bytes were
- peeked and allowEndOfInput is false.
-
IOException - If an error occurs peeking from the input.
Peeks length bytes from the peek position, writing them into target at index
- offset. The current read position is left unchanged.
-
- Calling ExtractorInput.resetPeekPosition() resets the peek position to equal the current read
- position, so the caller can peek the same data again. Reading or skipping also resets the peek
- position.
- If the end of the input is encountered before advancing the peek position, then behavior is
- dependent on allowEndOfInput. If allowEndOfInput == true then false is
- returned. Otherwise an EOFException is thrown.
length - The number of bytes by which to advance the peek position.
-
allowEndOfInput - True if encountering the end of the input before advancing is allowed,
- and should result in false being returned. False if it should be considered an
- error, causing an EOFException to be thrown.
-
Returns:
-
True if advancing the peek position was successful. False if the end of the input was
- encountered before the peek position could be advanced.
-
Throws:
-
EOFException - If the end of input was encountered having partially advanced (i.e. having
- advanced by at least one byte, but fewer than length), or if the end of input was
- encountered before advancing and allowEndOfInput is false.
-
IOException - If an error occurs advancing the peek position.
public int sampleData(DataSource dataSource,
- int length,
- boolean allowEndOfInput)
- throws IOException
-
Invoked to write sample data to the output.
-
-
Parameters:
-
dataSource - A DataSource from which to read the sample data.
-
length - The maximum length to read from the input.
-
allowEndOfInput - True if encountering the end of the input having read no data is
- allowed, and should result in C.RESULT_END_OF_INPUT being returned. False if it
- should be considered an error, causing an EOFException to be thrown.
-
Returns:
-
The number of bytes appended.
-
Throws:
-
IOException - If an error occurred reading from the input.
input - An ExtractorInput from which to read the sample data.
-
length - The maximum length to read from the input.
-
allowEndOfInput - True if encountering the end of the input having read no data is
- allowed, and should result in C.RESULT_END_OF_INPUT being returned. False if it
- should be considered an error, causing an EOFException to be thrown.
-
Returns:
-
The number of bytes appended.
-
Throws:
-
IOException - If an error occurred reading from the input.
input - An ExtractorInput from which to read the sample data.
-
length - The maximum length to read from the input.
-
allowEndOfInput - True if encountering the end of the input having read no data is
- allowed, and should result in C.RESULT_END_OF_INPUT being returned. False if it
- should be considered an error, causing an EOFException to be thrown.
-
Returns:
-
The number of bytes appended.
-
Throws:
-
IOException - If an error occurred reading from the input.
- A single call to this method will block until some progress has been made, but will not block
- for longer than this. Hence each call will consume only a small amount of input data.
-
- In the common case, RESULT_CONTINUE is returned to indicate that the
- ExtractorInput passed to the next read is required to provide data continuing from the
- position in the stream reached by the returning call. If the extractor requires data to be
- provided from a different position, then that position is set in seekPosition and
- RESULT_SEEK is returned. If the extractor reached the end of the data provided by the
- ExtractorInput, then RESULT_END_OF_INPUT is returned.
-
-
Parameters:
-
input - The ExtractorInput from which data should be read.
-
seekPosition - If RESULT_SEEK is returned, this holder is updated to hold the
- position of the required data.
-
Returns:
-
One of the RESULT_ values defined in this interface.
-
Throws:
-
IOException - If an error occurred reading from the input.
Notifies the extractor that a seek has occurred.
-
- Following a call to this method, the ExtractorInput passed to the next invocation of
- read(ExtractorInput, PositionHolder) is required to provide data starting from a
- random access position in the stream. Valid random access positions are the start of the
- stream and positions that can be obtained from any SeekMap passed to the
- ExtractorOutput.
- If the end of the input is found having read no data, then behavior is dependent on
- allowEndOfInput. If allowEndOfInput == true then false is returned.
- Otherwise an EOFException is thrown.
-
- Encountering the end of input having partially satisfied the read is always considered an
- error, and will result in an EOFException being thrown.
-
-
Parameters:
-
target - A target array into which data should be written.
-
offset - The offset into the target array at which to write.
-
length - The number of bytes to read from the input.
-
allowEndOfInput - True if encountering the end of the input having read no data is
- allowed, and should result in false being returned. False if it should be
- considered an error, causing an EOFException to be thrown.
-
Returns:
-
True if the read was successful. False if the end of the input was encountered having
- read no data.
-
Throws:
-
EOFException - If the end of input was encountered having partially satisfied the read
- (i.e. having read at least one byte, but fewer than length), or if no bytes were
- read and allowEndOfInput is false.
-
IOException - If an error occurs reading from the input.
length - The number of bytes to skip from the input.
-
allowEndOfInput - True if encountering the end of the input having skipped no data is
- allowed, and should result in false being returned. False if it should be
- considered an error, causing an EOFException to be thrown.
-
Returns:
-
True if the skip was successful. False if the end of the input was encountered having
- skipped no data.
-
Throws:
-
EOFException - If the end of input was encountered having partially satisfied the skip
- (i.e. having skipped at least one byte, but fewer than length), or if no bytes were
- skipped and allowEndOfInput is false.
-
IOException - If an error occurs reading from the input.
boolean peekFully(byte[] target,
- int offset,
- int length,
- boolean allowEndOfInput)
- throws IOException,
- InterruptedException
-
Peeks length bytes from the peek position, writing them into target at index
- offset. The current read position is left unchanged.
-
- If the end of the input is found having peeked no data, then behavior is dependent on
- allowEndOfInput. If allowEndOfInput == true then false is returned.
- Otherwise an EOFException is thrown.
-
- Calling resetPeekPosition() resets the peek position to equal the current read
- position, so the caller can peek the same data again. Reading and skipping also reset the peek
- position.
-
-
Parameters:
-
target - A target array into which data should be written.
-
offset - The offset into the target array at which to write.
-
length - The number of bytes to peek from the input.
-
allowEndOfInput - True if encountering the end of the input having peeked no data is
- allowed, and should result in false being returned. False if it should be
- considered an error, causing an EOFException to be thrown.
-
Returns:
-
True if the peek was successful. False if the end of the input was encountered having
- peeked no data.
-
Throws:
-
EOFException - If the end of input was encountered having partially satisfied the peek
- (i.e. having peeked at least one byte, but fewer than length), or if no bytes were
- peeked and allowEndOfInput is false.
-
IOException - If an error occurs peeking from the input.
Peeks length bytes from the peek position, writing them into target at index
- offset. The current read position is left unchanged.
-
- Calling resetPeekPosition() resets the peek position to equal the current read
- position, so the caller can peek the same data again. Reading or skipping also resets the peek
- position.
-
-
Parameters:
-
target - A target array into which data should be written.
-
offset - The offset into the target array at which to write.
-
length - The number of bytes to peek from the input.
-
Throws:
-
EOFException - If the end of input was encountered.
-
IOException - If an error occurs peeking from the input.
- If the end of the input is encountered before advancing the peek position, then behavior is
- dependent on allowEndOfInput. If allowEndOfInput == true then false is
- returned. Otherwise an EOFException is thrown.
-
-
Parameters:
-
length - The number of bytes by which to advance the peek position.
-
allowEndOfInput - True if encountering the end of the input before advancing is allowed,
- and should result in false being returned. False if it should be considered an
- error, causing an EOFException to be thrown.
-
Returns:
-
True if advancing the peek position was successful. False if the end of the input was
- encountered before the peek position could be advanced.
-
Throws:
-
EOFException - If the end of input was encountered having partially advanced (i.e. having
- advanced by at least one byte, but fewer than length), or if the end of input was
- encountered before advancing and allowEndOfInput is false.
-
IOException - If an error occurs advancing the peek position.
If no Extractor instances are passed to the constructor, the input stream container
- format will be detected automatically from the following supported formats:
-
-
FLAC (only available if the FLAC extension is built and included)
-
-
-
Seeking in AAC, MPEG TS and FLV streams is not supported.
-
-
To override the default extractors, pass one or more Extractor instances to the
- constructor. When reading a new stream, the first Extractor that returns true
- from Extractor.sniff(ExtractorInput) will be used.
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
dataSource - A data source to read the media stream.
-
allocator - An Allocator from which to obtain memory allocations.
-
requestedBufferSize - The requested total buffer size for storing sample data, in bytes.
- The actual allocated size may exceed the value passed in if the implementation requires it.
-
extractors - Extractors to extract the media stream, in order of decreasing
- priority. If omitted, the default extractors will be used.
dataSource - A data source to read the media stream.
-
allocator - An Allocator from which to obtain memory allocations.
-
requestedBufferSize - The requested total buffer size for storing sample data, in bytes.
- The actual allocated size may exceed the value passed in if the implementation requires it.
-
eventHandler - A handler to use when delivering events to eventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
-
eventSourceId - An identifier that gets passed to eventListener methods.
-
extractors - Extractors to extract the media stream, in order of decreasing
- priority. If omitted, the default extractors will be used.
dataSource - A data source to read the media stream.
-
allocator - An Allocator from which to obtain memory allocations.
-
requestedBufferSize - The requested total buffer size for storing sample data, in bytes.
- The actual allocated size may exceed the value passed in if the implementation requires it.
-
minLoadableRetryCount - The minimum number of times that the sample source will retry
- if a loading error occurs.
-
extractors - Extractors to extract the media stream, in order of decreasing
- priority. If omitted, the default extractors will be used.
dataSource - A data source to read the media stream.
-
allocator - An Allocator from which to obtain memory allocations.
-
requestedBufferSize - The requested total buffer size for storing sample data, in bytes.
- The actual allocated size may exceed the value passed in if the implementation requires it.
-
minLoadableRetryCount - The minimum number of times that the sample source will retry
- if a loading error occurs.
-
eventHandler - A handler to use when delivering events to eventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
-
eventSourceId - An identifier that gets passed to eventListener methods.
-
extractors - Extractors to extract the media stream, in order of decreasing
- priority. If omitted, the default extractors will be used.
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
-
- Preparation may require reading from the data source (e.g. to determine the available tracks
- and formats). If insufficient data is available then the call will return false
- rather than block. The method can be called repeatedly until the return value indicates
- success.
- Note that whilst the format of a track will remain constant, the format of the actual media
- stream may change dynamically. An example of this is where the track is adaptive
- (i.e. @link MediaFormat.adaptive is true). Hence the track formats returned through
- this method should not be used to configure decoders. Decoder configuration should be
- performed using the formats obtained when reading the media stream through calls to
- SampleSource.SampleSourceReader.readData(int, long, MediaFormatHolder, SampleHolder).
-
- This method should only be called after the source has been prepared.
playbackPositionUs - The current playback position.
-
Returns:
-
True if the track has available samples, or if the end of the stream has been
- reached. False if more data needs to be buffered for samples to become available.
Attempts to read a sample or a new format from the source.
-
- This method should only be called when the specified track is enabled.
-
- Note that where multiple tracks are enabled, SampleSource.NOTHING_READ may be returned if the
- next piece of data to be read from the SampleSource corresponds to a different track
- than the one for which data was requested.
-
playbackPositionUs - The current playback position.
-
formatHolder - A MediaFormatHolder object to populate in the case of a new
- format.
-
sampleHolder - A SampleHolder object to populate in the case of a new sample.
- If the caller requires the sample data then it must ensure that SampleHolder.data
- references a valid output buffer.
- If seeking is not supported then the only valid seek position is the start of the file, and so
- getPosition(long) will return 0 for all input values.
input - An ExtractorInput from which to read the sample data.
-
length - The maximum length to read from the input.
-
allowEndOfInput - True if encountering the end of the input having read no data is
- allowed, and should result in C.RESULT_END_OF_INPUT being returned. False if it
- should be considered an error, causing an EOFException to be thrown.
-
Returns:
-
The number of bytes appended.
-
Throws:
-
IOException - If an error occurred reading from the input.
Notifies the extractor that a seek has occurred.
-
- Following a call to this method, the ExtractorInput passed to the next invocation of
- Extractor.read(ExtractorInput, PositionHolder) is required to provide data starting from a
- random access position in the stream. Valid random access positions are the start of the
- stream and positions that can be obtained from any SeekMap passed to the
- ExtractorOutput.
- A single call to this method will block until some progress has been made, but will not block
- for longer than this. Hence each call will consume only a small amount of input data.
-
- In the common case, Extractor.RESULT_CONTINUE is returned to indicate that the
- ExtractorInput passed to the next read is required to provide data continuing from the
- position in the stream reached by the returning call. If the extractor requires data to be
- provided from a different position, then that position is set in seekPosition and
- Extractor.RESULT_SEEK is returned. If the extractor reached the end of the data provided by the
- ExtractorInput, then Extractor.RESULT_END_OF_INPUT is returned.
- If seeking is not supported then the only valid seek position is the start of the file, and so
- SeekMap.getPosition(long) will return 0 for all input values.
Notifies the extractor that a seek has occurred.
-
- Following a call to this method, the ExtractorInput passed to the next invocation of
- Extractor.read(ExtractorInput, PositionHolder) is required to provide data starting from a
- random access position in the stream. Valid random access positions are the start of the
- stream and positions that can be obtained from any SeekMap passed to the
- ExtractorOutput.
- A single call to this method will block until some progress has been made, but will not block
- for longer than this. Hence each call will consume only a small amount of input data.
-
- In the common case, Extractor.RESULT_CONTINUE is returned to indicate that the
- ExtractorInput passed to the next read is required to provide data continuing from the
- position in the stream reached by the returning call. If the extractor requires data to be
- provided from a different position, then that position is set in seekPosition and
- Extractor.RESULT_SEEK is returned. If the extractor reached the end of the data provided by the
- ExtractorInput, then Extractor.RESULT_END_OF_INPUT is returned.
public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
-
Flag to work around an issue in some video streams where every frame is marked as a sync frame.
- The workaround overrides the sync frame flags in the stream, forcing them to false except for
- the first sample in each segment.
-
- This flag does nothing if the stream is not a video stream.
Notifies the extractor that a seek has occurred.
-
- Following a call to this method, the ExtractorInput passed to the next invocation of
- Extractor.read(ExtractorInput, PositionHolder) is required to provide data starting from a
- random access position in the stream. Valid random access positions are the start of the
- stream and positions that can be obtained from any SeekMap passed to the
- ExtractorOutput.
- A single call to this method will block until some progress has been made, but will not block
- for longer than this. Hence each call will consume only a small amount of input data.
-
- In the common case, Extractor.RESULT_CONTINUE is returned to indicate that the
- ExtractorInput passed to the next read is required to provide data continuing from the
- position in the stream reached by the returning call. If the extractor requires data to be
- provided from a different position, then that position is set in seekPosition and
- Extractor.RESULT_SEEK is returned. If the extractor reached the end of the data provided by the
- ExtractorInput, then Extractor.RESULT_END_OF_INPUT is returned.
Notifies the extractor that a seek has occurred.
-
- Following a call to this method, the ExtractorInput passed to the next invocation of
- Extractor.read(ExtractorInput, PositionHolder) is required to provide data starting from a
- random access position in the stream. Valid random access positions are the start of the
- stream and positions that can be obtained from any SeekMap passed to the
- ExtractorOutput.
- A single call to this method will block until some progress has been made, but will not block
- for longer than this. Hence each call will consume only a small amount of input data.
-
- In the common case, Extractor.RESULT_CONTINUE is returned to indicate that the
- ExtractorInput passed to the next read is required to provide data continuing from the
- position in the stream reached by the returning call. If the extractor requires data to be
- provided from a different position, then that position is set in seekPosition and
- Extractor.RESULT_SEEK is returned. If the extractor reached the end of the data provided by the
- ExtractorInput, then Extractor.RESULT_END_OF_INPUT is returned.
- If seeking is not supported then the only valid seek position is the start of the file, and so
- SeekMap.getPosition(long) will return 0 for all input values.
public static byte[] parseSchemeSpecificData(byte[] atom,
- UUID uuid)
-
Parses the scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are supported.
-
- The scheme specific data is only parsed if the data is a valid PSSH atom matching the given
- UUID, or if the data is a valid PSSH atom of any type in the case that the passed UUID is null.
-
-
Parameters:
-
atom - The atom to parse.
-
uuid - The required UUID of the PSSH atom, or null to accept any UUID.
-
Returns:
-
The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the
- PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID.
public Track(int id,
- int type,
- long timescale,
- long movieTimescale,
- long durationUs,
- MediaFormat mediaFormat,
- TrackEncryptionBox[] sampleDescriptionEncryptionBoxes,
- int nalUnitLengthFieldLength,
- long[] editListDurations,
- long[] editListMediaTimes)
Notifies the extractor that a seek has occurred.
-
- Following a call to this method, the ExtractorInput passed to the next invocation of
- Extractor.read(ExtractorInput, PositionHolder) is required to provide data starting from a
- random access position in the stream. Valid random access positions are the start of the
- stream and positions that can be obtained from any SeekMap passed to the
- ExtractorOutput.
- A single call to this method will block until some progress has been made, but will not block
- for longer than this. Hence each call will consume only a small amount of input data.
-
- In the common case, Extractor.RESULT_CONTINUE is returned to indicate that the
- ExtractorInput passed to the next read is required to provide data continuing from the
- position in the stream reached by the returning call. If the extractor requires data to be
- provided from a different position, then that position is set in seekPosition and
- Extractor.RESULT_SEEK is returned. If the extractor reached the end of the data provided by the
- ExtractorInput, then Extractor.RESULT_END_OF_INPUT is returned.
Notifies the extractor that a seek has occurred.
-
- Following a call to this method, the ExtractorInput passed to the next invocation of
- Extractor.read(ExtractorInput, PositionHolder) is required to provide data starting from a
- random access position in the stream. Valid random access positions are the start of the
- stream and positions that can be obtained from any SeekMap passed to the
- ExtractorOutput.
- A single call to this method will block until some progress has been made, but will not block
- for longer than this. Hence each call will consume only a small amount of input data.
-
- In the common case, Extractor.RESULT_CONTINUE is returned to indicate that the
- ExtractorInput passed to the next read is required to provide data continuing from the
- position in the stream reached by the returning call. If the extractor requires data to be
- provided from a different position, then that position is set in seekPosition and
- Extractor.RESULT_SEEK is returned. If the extractor reached the end of the data provided by the
- ExtractorInput, then Extractor.RESULT_END_OF_INPUT is returned.
Notifies the extractor that a seek has occurred.
-
- Following a call to this method, the ExtractorInput passed to the next invocation of
- Extractor.read(ExtractorInput, PositionHolder) is required to provide data starting from a
- random access position in the stream. Valid random access positions are the start of the
- stream and positions that can be obtained from any SeekMap passed to the
- ExtractorOutput.
- A single call to this method will block until some progress has been made, but will not block
- for longer than this. Hence each call will consume only a small amount of input data.
-
- In the common case, Extractor.RESULT_CONTINUE is returned to indicate that the
- ExtractorInput passed to the next read is required to provide data continuing from the
- position in the stream reached by the returning call. If the extractor requires data to be
- provided from a different position, then that position is set in seekPosition and
- Extractor.RESULT_SEEK is returned. If the extractor reached the end of the data provided by the
- ExtractorInput, then Extractor.RESULT_END_OF_INPUT is returned.
public PtsTimestampAdjuster(long firstSampleTimestampUs)
-
-
Parameters:
-
firstSampleTimestampUs - The desired result of the first call to
- adjustTimestamp(long), or DO_NOT_OFFSET if presentation timestamps
- should not be offset.
Notifies the extractor that a seek has occurred.
-
- Following a call to this method, the ExtractorInput passed to the next invocation of
- Extractor.read(ExtractorInput, PositionHolder) is required to provide data starting from a
- random access position in the stream. Valid random access positions are the start of the
- stream and positions that can be obtained from any SeekMap passed to the
- ExtractorOutput.
- A single call to this method will block until some progress has been made, but will not block
- for longer than this. Hence each call will consume only a small amount of input data.
-
- In the common case, Extractor.RESULT_CONTINUE is returned to indicate that the
- ExtractorInput passed to the next read is required to provide data continuing from the
- position in the stream reached by the returning call. If the extractor requires data to be
- provided from a different position, then that position is set in seekPosition and
- Extractor.RESULT_SEEK is returned. If the extractor reached the end of the data provided by the
- ExtractorInput, then Extractor.RESULT_END_OF_INPUT is returned.
Notifies the extractor that a seek has occurred.
-
- Following a call to this method, the ExtractorInput passed to the next invocation of
- Extractor.read(ExtractorInput, PositionHolder) is required to provide data starting from a
- random access position in the stream. Valid random access positions are the start of the
- stream and positions that can be obtained from any SeekMap passed to the
- ExtractorOutput.
- A single call to this method will block until some progress has been made, but will not block
- for longer than this. Hence each call will consume only a small amount of input data.
-
- In the common case, Extractor.RESULT_CONTINUE is returned to indicate that the
- ExtractorInput passed to the next read is required to provide data continuing from the
- position in the stream reached by the returning call. If the extractor requires data to be
- provided from a different position, then that position is set in seekPosition and
- Extractor.RESULT_SEEK is returned. If the extractor reached the end of the data provided by the
- ExtractorInput, then Extractor.RESULT_END_OF_INPUT is returned.
- If seeking is not supported then the only valid seek position is the start of the file, and so
- SeekMap.getPosition(long) will return 0 for all input values.
public final class WebmExtractor
-extends Object
-implements Extractor
-
An extractor to facilitate data retrieval from the WebM container format.
-
- WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
- Matroska is available here.
- More info about WebM is here.
- RFC on encrypted WebM can be found
- here.
Notifies the extractor that a seek has occurred.
-
- Following a call to this method, the ExtractorInput passed to the next invocation of
- Extractor.read(ExtractorInput, PositionHolder) is required to provide data starting from a
- random access position in the stream. Valid random access positions are the start of the
- stream and positions that can be obtained from any SeekMap passed to the
- ExtractorOutput.
- A single call to this method will block until some progress has been made, but will not block
- for longer than this. Hence each call will consume only a small amount of input data.
-
- In the common case, Extractor.RESULT_CONTINUE is returned to indicate that the
- ExtractorInput passed to the next read is required to provide data continuing from the
- position in the stream reached by the returning call. If the extractor requires data to be
- provided from a different position, then that position is set in seekPosition and
- Extractor.RESULT_SEEK is returned. If the extractor reached the end of the data provided by the
- ExtractorInput, then Extractor.RESULT_END_OF_INPUT is returned.
isMaster - True if this is the master source for the playback. False otherwise. Each
- playback must have exactly one master source, which should be the source providing video
- chunks (or audio chunks for audio only playbacks).
-
dataSource - A DataSource suitable for loading the media data.
-
playlist - The HLS playlist.
-
trackSelector - Selects tracks to be exposed by this source.
-
bandwidthMeter - Provides an estimate of the currently available bandwidth.
-
timestampAdjusterProvider - A provider of PtsTimestampAdjuster instances. If
- multiple HlsChunkSources are used for a single playback, they should all share the
- same provider.
isMaster - True if this is the master source for the playback. False otherwise. Each
- playback must have exactly one master source, which should be the source providing video
- chunks (or audio chunks for audio only playbacks).
-
dataSource - A DataSource suitable for loading the media data.
-
playlist - The HLS playlist.
-
trackSelector - Selects tracks to be exposed by this source.
-
bandwidthMeter - Provides an estimate of the currently available bandwidth.
-
timestampAdjusterProvider - A provider of PtsTimestampAdjuster instances. If
- multiple HlsChunkSources are used for a single playback, they should all share the
- same provider.
-
minBufferDurationToSwitchUpMs - The minimum duration of media that needs to be buffered
- for a switch to a higher quality variant to be considered.
-
maxBufferDurationToSwitchDownMs - The maximum duration of media that needs to be buffered
- for a switch to a lower quality variant to be considered.
isMaster - True if this is the master source for the playback. False otherwise. Each
- playback must have exactly one master source, which should be the source providing video
- chunks (or audio chunks for audio only playbacks).
-
dataSource - A DataSource suitable for loading the media data.
-
playlist - The HLS playlist.
-
trackSelector - Selects tracks to be exposed by this source.
-
bandwidthMeter - Provides an estimate of the currently available bandwidth.
-
timestampAdjusterProvider - A provider of PtsTimestampAdjuster instances. If
- multiple HlsChunkSources are used for a single playback, they should all share the
- same provider.
-
minBufferDurationToSwitchUpMs - The minimum duration of media that needs to be buffered
- for a switch to a higher quality variant to be considered.
-
maxBufferDurationToSwitchDownMs - The maximum duration of media that needs to be buffered
- for a switch to a lower quality variant to be considered.
-
eventHandler - A handler to use when delivering events to eventListener. May be
- null if delivery of events is not required.
-
eventListener - A listener of events. May be null if delivery of events is not required.
previousTsChunk - The previously loaded chunk that the next chunk should follow.
-
playbackPositionUs - The current playback position. If previousTsChunk is null then this
- parameter is the position from which playback is expected to start (or restart) and hence
- should be interpreted as a seek position.
HlsExtractorWrapper(int trigger,
- Format format,
- long startTimeUs,
- Extractor extractor,
- boolean shouldSpliceIn,
- int adaptiveMaxWidth,
- int adaptiveMaxHeight)
public HlsExtractorWrapper(int trigger,
- Format format,
- long startTimeUs,
- Extractor extractor,
- boolean shouldSpliceIn,
- int adaptiveMaxWidth,
- int adaptiveMaxHeight)
Attempts to configure a splice from this extractor to the next.
-
- The splice is performed such that for each track the samples read from the next extractor
- start with a keyframe, and continue from where the samples read from this extractor finish.
- A successful splice may discard samples from either or both extractors.
-
- Splice configuration may fail if the next extractor is not yet in a state that allows the
- splice to be performed. Calling this method is a noop if the splice has already been
- configured. Hence this method should be called repeatedly during the window within which a
- splice can be performed.
-
- This method must only be called after the extractor has been prepared.
public HlsMediaPlaylist(String baseUri,
- int mediaSequence,
- int targetDurationSecs,
- int version,
- boolean live,
- List<HlsMediaPlaylist.Segment> segments)
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
-
- Preparation may require reading from the data source (e.g. to determine the available tracks
- and formats). If insufficient data is available then the call will return false
- rather than block. The method can be called repeatedly until the return value indicates
- success.
- Note that whilst the format of a track will remain constant, the format of the actual media
- stream may change dynamically. An example of this is where the track is adaptive
- (i.e. @link MediaFormat.adaptive is true). Hence the track formats returned through
- this method should not be used to configure decoders. Decoder configuration should be
- performed using the formats obtained when reading the media stream through calls to
- SampleSource.SampleSourceReader.readData(int, long, MediaFormatHolder, SampleHolder).
-
- This method should only be called after the source has been prepared.
playbackPositionUs - The current playback position.
-
Returns:
-
True if the track has available samples, or if the end of the stream has been
- reached. False if more data needs to be buffered for samples to become available.
Attempts to read a sample or a new format from the source.
-
- This method should only be called when the specified track is enabled.
-
- Note that where multiple tracks are enabled, SampleSource.NOTHING_READ may be returned if the
- next piece of data to be read from the SampleSource corresponds to a different track
- than the one for which data was requested.
-
playbackPositionUs - The current playback position.
-
formatHolder - A MediaFormatHolder object to populate in the case of a new
- format.
-
sampleHolder - A SampleHolder object to populate in the case of a new sample.
- If the caller requires the sample data then it must ensure that SampleHolder.data
- references a valid output buffer.
TsChunk(DataSource dataSource,
- DataSpec dataSpec,
- int trigger,
- Format format,
- long startTimeUs,
- long endTimeUs,
- int chunkIndex,
- int discontinuitySequenceNumber,
- HlsExtractorWrapper extractorWrapper,
- byte[] encryptionKey,
- byte[] encryptionIv)
public TsChunk(DataSource dataSource,
- DataSpec dataSpec,
- int trigger,
- Format format,
- long startTimeUs,
- long endTimeUs,
- int chunkIndex,
- int discontinuitySequenceNumber,
- HlsExtractorWrapper extractorWrapper,
- byte[] encryptionKey,
- byte[] encryptionIv)
source - A source from which samples containing metadata can be read.
-
metadataParser - A parser for parsing the metadata.
-
metadataRenderer - The metadata renderer to receive the parsed metadata.
-
metadataRendererLooper - The looper associated with the thread on which metadataRenderer
- should be invoked. If the renderer makes use of standard Android UI components, then this
- should normally be the looper associated with the applications' main thread, which can be
- obtained using ContextWrapper.getMainLooper(). Null may be passed if the
- renderer should be invoked directly on the player's internal rendering thread.
Whether the renderer is ready for the ExoPlayer instance to transition to
- ExoPlayer.STATE_ENDED. The player will make this transition as soon as true is
- returned by all of its TrackRenderers.
-
Whether the renderer is able to immediately render media from the current position.
-
- If the renderer is in the TrackRenderer.STATE_STARTED state then returning true indicates that the
- renderer has everything that it needs to continue playback. Returning false indicates that
- the player should pause until the renderer is ready.
-
- If the renderer is in the TrackRenderer.STATE_ENABLED state then returning true indicates that the
- renderer is ready for playback to be started. Returning false indicates that it is not.
-
A LoadControl implementation that allows loads to continue in a sequence that prevents
- any loader from getting too far ahead or behind any of the other loaders.
context - A context. May be null if filterVideoRepresentations == false.
-
filterVideoRepresentations - Whether video representations should be filtered according to
- the capabilities of the device. It is strongly recommended to set this to true,
- unless the application has already verified that all representations are playable.
-
filterProtectedHdContent - Whether video representations that are both drm protected and
- high definition should be filtered when tracks are built. If
- filterVideoRepresentations == false then this parameter is ignored.
- May also be used for fixed duration content, in which case the call is equivalent to calling
- the other constructor, passing manifestFetcher.getManifest() is the first argument.
-
-
Parameters:
-
manifestFetcher - A fetcher for the manifest, which must have already successfully
- completed an initial load.
-
trackSelector - Selects tracks from the manifest to be exposed by this source.
-
dataSource - A DataSource suitable for loading the media data.
-
adaptiveFormatEvaluator - For adaptive tracks, selects from the available formats.
-
liveEdgeLatencyMs - For live streams, the number of milliseconds that the playback should
- lag behind the "live edge" (i.e. the end of the most recently defined media in the
- manifest). Choosing a small value will minimize latency introduced by the player, however
- note that the value sets an upper bound on the length of media that the player can buffer.
- Hence a small value may increase the probability of rebuffering and playback failures.
queue - A representation of the currently buffered MediaChunks.
-
playbackPositionUs - The current playback position. If the queue is empty then this
- parameter is the position from which playback is expected to start (or restart) and hence
- should be interpreted as a seek position.
-
out - A holder for the next operation, whose ChunkOperationHolder.endOfStream is
- initially set to false, whose ChunkOperationHolder.queueSize is initially equal to
- the length of the queue, and whose ChunkOperationHolder.chunk is initially equal to
- null or a Chunk previously supplied by the ChunkSource that the caller has
- not yet finished loading. In the latter case the chunk can either be replaced or left
- unchanged. Note that leaving the chunk unchanged is both preferred and more efficient than
- replacing it with a new but identical chunk.
public StreamElement(String baseUri,
- String chunkTemplate,
- int type,
- String subType,
- long timescale,
- String name,
- int qualityLevels,
- int maxWidth,
- int maxHeight,
- int displayWidth,
- int displayHeight,
- String language,
- SmoothStreamingManifest.TrackElement[] tracks,
- List<Long> chunkStartTimes,
- long lastChunkDuration)
public TrackElement(int index,
- int bitrate,
- String mimeType,
- byte[][] csd,
- int maxWidth,
- int maxHeight,
- int sampleRate,
- int numChannels,
- String language)
The length of the trailing window for a live broadcast in microseconds, or
- C.UNKNOWN_TIME_US if the stream is not live or if the window length is unspecified.
The length of the trailing window for a live broadcast in microseconds, or
- C.UNKNOWN_TIME_US if the stream is not live or if the window length is unspecified.
The position of the lineAnchor of the cue box within the viewport in the direction
- orthogonal to the writing direction, or DIMEN_UNSET. When set, the interpretation of
- the value depends on the value of lineType.
-
- For horizontal text and lineType equal to LINE_TYPE_FRACTION, this is the
- fractional vertical position relative to the top of the viewport.
- LINE_TYPE_FRACTION indicates that line is a fractional position within the
- viewport.
-
- LINE_TYPE_NUMBER indicates that line is a line number, where the size of each
- line is taken to be the size of the first line of the cue. When line is greater than
- or equal to 0, lines count from the start of the viewport (the first line is numbered 0). When
- line is negative, lines count from the end of the viewport (the last line is numbered
- -1). For horizontal text the size of the first line of the cue is its height, and the start
- and end of the viewport are the top and bottom respectively.
The fractional position of the positionAnchor of the cue box within the viewport in
- the direction orthogonal to line, or DIMEN_UNSET.
-
- For horizontal text, this is the horizontal position relative to the left of the viewport. Note
- that positioning is relative to the left of the viewport even in the case of right-to-left
- text.
Sets the bottom padding fraction to apply when Cue.line is Cue.DIMEN_UNSET,
- as a fraction of the view's remaining height after its top and bottom padding have been
- subtracted.
public void setFractionalTextSize(float fractionOfHeight,
- boolean ignorePadding)
-
Sets the text size to be a fraction of the height of this view.
-
-
Parameters:
-
fractionOfHeight - A fraction between 0 and 1.
-
ignorePadding - Set to true if fractionOfHeight should be interpreted as a
- fraction of this view's height ignoring any top and bottom padding. Set to false if
- fractionOfHeight should be interpreted as a fraction of this view's remaining
- height after the top and bottom padding has been subtracted.
public void setBottomPaddingFraction(float bottomPaddingFraction)
-
Sets the bottom padding fraction to apply when Cue.line is Cue.DIMEN_UNSET,
- as a fraction of the view's remaining height after its top and bottom padding have been
- subtracted.
-
- Note that this padding is applied in addition to any standard view padding.
-
-
Parameters:
-
bottomPaddingFraction - The bottom padding fraction.
A TrackRenderer for subtitles. Text is parsed from sample data using a
- SubtitleParser. The actual rendering of each line of text is delegated to a
- TextRenderer.
-
- If no SubtitleParser instances are passed to the constructor, the subtitle type will be
- detected automatically for the following supported formats:
-
-
source - A source from which samples containing subtitle data can be read.
-
textRenderer - The text renderer.
-
textRendererLooper - The looper associated with the thread on which textRenderer should be
- invoked. If the renderer makes use of standard Android UI components, then this should
- normally be the looper associated with the applications' main thread, which can be
- obtained using ContextWrapper.getMainLooper(). Null may be passed if the
- renderer should be invoked directly on the player's internal rendering thread.
-
subtitleParsers - SubtitleParsers to parse text samples, in order of decreasing
- priority. If omitted, the default parsers will be used.
sources - Sources from which samples containing subtitle data can be read.
-
textRenderer - The text renderer.
-
textRendererLooper - The looper associated with the thread on which textRenderer should be
- invoked. If the renderer makes use of standard Android UI components, then this should
- normally be the looper associated with the applications' main thread, which can be
- obtained using ContextWrapper.getMainLooper(). Null may be passed if the
- renderer should be invoked directly on the player's internal rendering thread.
-
subtitleParsers - SubtitleParsers to parse text samples, in order of decreasing
- priority. If omitted, the default parsers will be used.
Whether the renderer is ready for the ExoPlayer instance to transition to
- ExoPlayer.STATE_ENDED. The player will make this transition as soon as true is
- returned by all of its TrackRenderers.
-
Whether the renderer is able to immediately render media from the current position.
-
- If the renderer is in the TrackRenderer.STATE_STARTED state then returning true indicates that the
- renderer has everything that it needs to continue playback. Returning false indicates that
- the player should pause until the renderer is ready.
-
- If the renderer is in the TrackRenderer.STATE_ENABLED state then returning true indicates that the
- renderer is ready for playback to be started. Returning false indicates that it is not.
-
source - A source from which samples containing EIA-608 closed captions can be read.
-
textRenderer - The text renderer.
-
textRendererLooper - The looper associated with the thread on which textRenderer should be
- invoked. If the renderer makes use of standard Android UI components, then this should
- normally be the looper associated with the applications' main thread, which can be
- obtained using ContextWrapper.getMainLooper(). Null may be passed if the
- renderer should be invoked directly on the player's internal rendering thread.
Whether the renderer is ready for the ExoPlayer instance to transition to
- ExoPlayer.STATE_ENDED. The player will make this transition as soon as true is
- returned by all of its TrackRenderers.
-
Whether the renderer is able to immediately render media from the current position.
-
- If the renderer is in the TrackRenderer.STATE_STARTED state then returning true indicates that the
- renderer has everything that it needs to continue playback. Returning false indicates that
- the player should pause until the renderer is ready.
-
- If the renderer is in the TrackRenderer.STATE_ENABLED state then returning true indicates that the
- renderer is ready for playback to be started. Returning false indicates that it is not.
-
Reads lines up to and including the next WebVTT cue header.
-
-
Parameters:
-
input - The input from which lines should be read.
-
Returns:
-
A Matcher for the WebVTT cue header, or null if the end of the input was
- reached without a cue header being found. In the case that a cue header is found, groups 1,
- 2 and 3 of the returned matcher contain the start time, end time and settings list.
The array containing the allocated space. The allocated space may not be at the start of the
- array, and so translateOffset(int) method must be used when indexing into it.
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
void onBandwidthSample(int elapsedMs,
- long bytes,
- long bitrate)
-
Invoked periodically to indicate that bytes have been transferred.
-
-
Parameters:
-
elapsedMs - The time taken to transfer the bytes, in milliseconds.
-
bytes - The number of bytes transferred.
-
bitrate - The estimated bitrate in bits/sec, or BandwidthMeter.NO_ESTIMATE if no estimate
- is available. Note that this estimate is typically derived from more information than
- bytes and elapsedMs.
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
-
Throws:
-
IOException - If an error occurs opening the source.
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
- Note: If open(DataSpec) throws an IOException, callers must still call
- close() to ensure that any partial effects of the open(DataSpec) invocation
- are cleaned up. Implementations of this class can assume that callers will call
- close() in this case.
-
-
Parameters:
-
dataSpec - Defines the data to be read.
-
Returns:
-
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
-
Throws:
-
IOException - If an error occurs opening the source.
Optional call to open the underlying DataSource.
-
- Calling this method does nothing if the DataSource is already open. Calling this
- method is optional, since the read and skip methods will automatically open the underlying
- DataSource if it's not open already.
Permits an underlying network stack to request that the server use gzip compression.
-
- Should not typically be set if the data being requested is already compressed (e.g. most audio
- and video requests). May be set when requesting other data.
-
A key that uniquely identifies the original stream. Used for cache indexing. May be null if the
- DataSpec is not intended to be used in conjunction with a cache.
- By default this implementation will not follow cross-protocol redirects (i.e. redirects from
- HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the
- DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean)
- constructor and passing true as the final argument.
public DefaultHttpDataSource(String userAgent,
- Predicate<String> contentTypePredicate,
- TransferListener listener,
- int connectTimeoutMillis,
- int readTimeoutMillis)
-
-
Parameters:
-
userAgent - The User-Agent string that should be used.
connectTimeoutMillis - The connection timeout, in milliseconds. A timeout of zero is
- interpreted as an infinite timeout. Pass DEFAULT_CONNECT_TIMEOUT_MILLIS to use
- the default value.
-
readTimeoutMillis - The read timeout, in milliseconds. A timeout of zero is interpreted
- as an infinite timeout. Pass DEFAULT_READ_TIMEOUT_MILLIS to use the default value.
-
allowCrossProtocolRedirects - Whether cross-protocol redirects (i.e. redirects from HTTP
- to HTTPS and vice versa) are enabled.
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
Returns the number of bytes that are still to be read for the current DataSpec.
-
- If the total length of the data being read is known, then this length minus bytesRead()
- is returned. If the total length is unknown, C.LENGTH_UNBOUNDED is returned.
public final class DefaultUriDataSource
-extends Object
-implements UriDataSource
-
A UriDataSource that supports multiple URI schemes. The supported schemes are:
-
-
-
http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4).
-
file: For fetching data from a local file (e.g. file:///path/to/media/media.mp4, or just
- /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is a
- local file URI).
-
asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4).
-
content: For fetching data from a content URI (e.g. content://authority/path/123).
-
public DefaultUriDataSource(Context context,
- String userAgent)
-
Constructs a new instance.
-
- The constructed instance will not follow cross-protocol redirects (i.e. redirects from HTTP to
- HTTPS or vice versa) when fetching remote data. Cross-protocol redirects can be enabled by
- using DefaultUriDataSource(Context, TransferListener, String, boolean) and passing
- true as the final argument.
-
-
Parameters:
-
context - A context.
-
userAgent - The User-Agent string that should be used when requesting remote data.
- The constructed instance will not follow cross-protocol redirects (i.e. redirects from HTTP to
- HTTPS or vice versa) when fetching remote data. Cross-protocol redirects can be enabled by
- using DefaultUriDataSource(Context, TransferListener, String, boolean) and passing
- true as the final argument.
userAgent - The User-Agent string that should be used when requesting remote data.
-
allowCrossProtocolRedirects - Whether cross-protocol redirects (i.e. redirects from HTTP
- to HTTPS and vice versa) are enabled when fetching remote data..
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
-
Throws:
-
IOException - If an error occurs opening the source.
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
- Manages different priority network tasks. A network task that wishes to have its priority
- respected, and respect the priority of other tasks, should register itself with the lock prior
- to making network requests. It should then call one of the lock's proceed methods frequently
- during execution, so as to ensure that it continues only if it is the highest (or equally
- highest) priority task.
-
- Note that lower integer values correspond to higher priorities.
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
-
Throws:
-
IOException - If an error occurs opening the source.
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
-
Throws:
-
IOException - If an error occurs opening the source.
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
Registers a listener to listen for changes to a given key.
-
- No guarantees are made about the thread or threads on which the listener is invoked, but it
- is guaranteed that listener methods will be invoked in a serial fashion (i.e. one at a time)
- and in the same order as events occurred.
A caller should invoke this method when they require data from a given position for a given
- key.
-
- If there is a cache entry that overlaps the position, then the returned CacheSpan
- defines the file in which the data is stored. CacheSpan.isCached is true. The caller
- may read from the cache file, but does not acquire any locks.
-
- If there is no cache entry overlapping offset, then the returned CacheSpan
- defines a hole in the cache starting at position into which the caller may write as it
- obtains the data from some other source. The returned CacheSpan serves as a lock.
- Whilst the caller holds the lock it may write data into the hole. It may split data into
- multiple files. When the caller has finished writing a file it should commit it to the cache
- by calling commitFile(File). When the caller has finished writing, it must release
- the lock by calling releaseHoleSpan(com.google.android.exoplayer.upstream.cache.CacheSpan).
-
-
Parameters:
-
key - The key of the data being requested.
-
position - The position of the data being requested.
Same as startReadWrite(String, long). However, if the cache entry is locked, then
- instead of blocking, this method will return null as the CacheSpan.
-
-
Parameters:
-
key - The key of the data being requested.
-
position - The position of the data being requested.
-
Returns:
-
The CacheSpan. Or null if the cache entry is locked.
Obtains a cache file into which data can be written. Must only be called when holding a
- corresponding hole CacheSpan obtained from startReadWrite(String, long).
-
-
Parameters:
-
key - The cache key for the data.
-
position - The starting position of the data.
-
length - The length of the data to be written. Used only to ensure that there is enough
- space in the cache.
public CacheDataSink(Cache cache,
- long maxCacheFileSize)
-
-
Parameters:
-
cache - The cache into which data should be written.
-
maxCacheFileSize - The maximum size of a cache file, in bytes. If the sink is opened for
- a DataSpec whose size exceeds this value, then the data will be fragmented into
- multiple cache files.
public CacheDataSink(Cache cache,
- long maxCacheFileSize,
- int bufferSize)
-
-
Parameters:
-
cache - The cache into which data should be written.
-
maxCacheFileSize - The maximum size of a cache file, in bytes. If the sink is opened for
- a DataSpec whose size exceeds this value, then the data will be fragmented into
- multiple cache files.
-
bufferSize - The buffer size in bytes for writing to a cache file. A zero or negative
- value disables buffering.
public final class CacheDataSource
-extends Object
-implements DataSource
-
A DataSource that reads and writes a Cache. Requests are fulfilled from the cache
- when possible. When data is not cached it is requested from an upstream DataSource and
- written into the cache.
public CacheDataSource(Cache cache,
- DataSource upstream,
- boolean blockOnCache,
- boolean ignoreCacheOnError,
- long maxCacheFileSize)
-
Constructs an instance with default DataSource and DataSink instances for
- reading and writing the cache. The sink is configured to fragment data such that no single
- cache file is greater than maxCacheFileSize bytes.
Constructs an instance with arbitrary DataSource and DataSink instances for
- reading and writing the cache. One use of this constructor is to allow data to be transformed
- before it is written to disk.
-
-
Parameters:
-
cache - The cache.
-
upstream - A DataSource for reading data not in the cache.
-
cacheReadDataSource - A DataSource for reading data from the cache.
-
cacheWriteDataSink - A DataSink for writing data to the cache.
-
blockOnCache - A flag indicating whether we will block reads if the cache key is locked.
- If this flag is false, then we will read from upstream if the cache key is locked.
-
ignoreCacheOnError - Whether the cache is bypassed following any cache related error. If
- true, then cache related exceptions may be thrown for one cycle of open, read and close
- calls. Subsequent cycles of these calls will then bypass the cache.
The number of bytes that can be read from the opened source. For unbounded requests
- (i.e. requests where DataSpec.length equals C.LENGTH_UNBOUNDED) this value
- is the resolved length of the request, or C.LENGTH_UNBOUNDED if the length is still
- unresolved. For all other requests, the value returned will be equal to the request's
- DataSpec.length.
-
Throws:
-
IOException - If an error occurs opening the source.
public final class NoOpCacheEvictor
-extends Object
-implements CacheEvictor
-
Evictor that doesn't ever evict cache files.
-
- Warning: Using this evictor might have unforeseeable consequences if cache
- size is not managed elsewhere.
Registers a listener to listen for changes to a given key.
-
- No guarantees are made about the thread or threads on which the listener is invoked, but it
- is guaranteed that listener methods will be invoked in a serial fashion (i.e. one at a time)
- and in the same order as events occurred.
A caller should invoke this method when they require data from a given position for a given
- key.
-
- If there is a cache entry that overlaps the position, then the returned CacheSpan
- defines the file in which the data is stored. CacheSpan.isCached is true. The caller
- may read from the cache file, but does not acquire any locks.
-
- If there is no cache entry overlapping offset, then the returned CacheSpan
- defines a hole in the cache starting at position into which the caller may write as it
- obtains the data from some other source. The returned CacheSpan serves as a lock.
- Whilst the caller holds the lock it may write data into the hole. It may split data into
- multiple files. When the caller has finished writing a file it should commit it to the cache
- by calling Cache.commitFile(File). When the caller has finished writing, it must release
- the lock by calling Cache.releaseHoleSpan(com.google.android.exoplayer.upstream.cache.CacheSpan).
public com.google.android.exoplayer.upstream.cache.SimpleCacheSpan startReadWriteNonBlocking(String key,
- long position)
- throws Cache.CacheException
Returns the AC-3 format given data containing the AC3SpecificBox according to
- ETSI TS 102 366 Annex F. The reading position of data will be modified.
-
-
Parameters:
-
data - The AC3SpecificBox to parse.
-
trackId - The track identifier to set on the format, or null.
-
durationUs - The duration to set on the format, in microseconds.
Returns the E-AC-3 format given data containing the EC3SpecificBox according to
- ETSI TS 102 366 Annex F. The reading position of data will be modified.
-
-
Parameters:
-
data - The EC3SpecificBox to parse.
-
trackId - The track identifier to set on the format, or null.
-
durationUs - The duration to set on the format, in microseconds.
A helper class for performing atomic operations on a file by creating a backup file until a write
- has successfully completed.
-
-
Atomic file guarantees file integrity by ensuring that a file has been completely written and
- sync'd to disk before removing its backup. As long as the backup file exists, the original file
- is considered to be invalid (left over from a previous attempt to write the file).
-
-
Atomic file does not confer any file locking semantics. Do not use this class when the file
- may be accessed or modified concurrently by multiple threads or processes. The caller is
- responsible for ensuring appropriate mutual exclusion invariants whenever it accesses the file.
Start a new write operation on the file. This returns an OutputStream to which you can
- write the new file data. If the whole data is written successfully you must call
- endWrite(OutputStream). On failure you should call OutputStream.close()
- only to free up resources used by it.
-
-
Note that if another thread is currently performing a write, this will simply replace
- whatever that thread is writing with the new file being written by this thread, and when the
- other thread finishes the write the new write operation will no longer be safe (or will be
- lost). You must do your own threading protection for access to AtomicFile.
Call when you have successfully finished writing to the stream returned by startWrite(). This will close, sync, and commit the new data. The next attempt to read the
- atomic file will return the new file stream.
-
-
Parameters:
-
str - Outer-most wrapper OutputStream used to write to the stream returned by startWrite().
Open the atomic file for reading. If there previously was an incomplete write, this will roll
- back to the last good data before opening for read.
-
-
Note that if another thread is currently performing a write, this will incorrectly consider
- it to be in the state of a bad write and roll back, causing the new data currently being
- written to be dropped. You must do your own threading protection for access to AtomicFile.
- If the input consists of NAL start code delimited units, then the returned array consists of
- the split NAL units, each of which is still prefixed with the NAL start code. For any other
- input, null is returned.
-
-
Parameters:
-
data - An array of data.
-
Returns:
-
The individual NAL units, or null if the input did not consist of NAL start code
- delimited units.
Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure.
-
-
-
-
FlacStreamInfo(int minBlockSize,
- int maxBlockSize,
- int minFrameSize,
- int maxFrameSize,
- int sampleRate,
- int channels,
- int bitsPerSample,
- long totalSamples)
public FlacStreamInfo(int minBlockSize,
- int maxBlockSize,
- int minFrameSize,
- int maxFrameSize,
- int sampleRate,
- int channels,
- int bitsPerSample,
- long totalSamples)
For on-demand playbacks, the loader is no longer required. For live playbacks, the loader
- may be required to periodically refresh the manifest. In this case it is injected into any
- components that require it. These components will call requestRefresh() on the
- loader whenever a refresh is required.
Theoretical maximum frame size for an MPEG audio stream, which occurs when playing a Layer 2
- MPEG 2.5 audio stream at 16 kb/s (with padding). The size is 1152 sample/frame *
- 160000 bit/s / (8000 sample/s * 8 bit/byte) + 1 padding byte/frame = 2881 byte/frame.
- The next power of two size is 4 KiB.
public SpsData(int seqParameterSetId,
- int width,
- int height,
- float pixelWidthAspectRatio,
- boolean separateColorPlaneFlag,
- boolean frameMbsOnlyFlag,
- int frameNumLength,
- int picOrderCountType,
- int picOrderCntLsbLength,
- boolean deltaPicOrderAlwaysZeroFlag)
public static int unescapeStream(byte[] data,
- int limit)
-
Unescapes data up to the specified limit, replacing occurrences of [0, 0, 3] with
- [0, 0]. The unescaped data is returned in-place, with the return value indicating its length.
-
- Executions of this method are mutually exclusive, so it should not be called with very large
- buffers.
-
-
Parameters:
-
data - The data to unescape.
-
limit - The limit (exclusive) of the data to unescape.
Discards data from the buffer up to the first SPS, where data.position() is interpreted
- as the length of the buffer.
-
- When the method returns, data.position() will contain the new length of the buffer. If
- the buffer is not empty it is guaranteed to start with an SPS.
-
-
Parameters:
-
data - Buffer containing start code delimited NAL units.
Parses a PPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection
- 7.3.2.2.
-
-
Parameters:
-
data - A ParsableBitArray containing the PPS data. The position must to set to the
- start of the data (i.e. the first bit of the pic_parameter_set_id field).
public static int findNalUnit(byte[] data,
- int startOffset,
- int endOffset,
- boolean[] prefixFlags)
-
Finds the first NAL unit in data.
-
- If prefixFlags is null then the first three bytes of a NAL unit must be entirely
- contained within the part of the array being searched in order for it to be found.
-
- When prefixFlags is non-null, this method supports finding NAL units whose first four
- bytes span data arrays passed to successive calls. To use this feature, pass the same
- prefixFlags parameter to successive calls. State maintained in this parameter enables
- the detection of such NAL units. Note that when using this feature, the return value may be 3,
- 2 or 1 less than startOffset, to indicate a NAL unit starting 3, 2 or 1 bytes before
- the first byte in the current array.
-
-
Parameters:
-
data - The data to search.
-
startOffset - The offset (inclusive) in the data to start the search.
-
endOffset - The offset (exclusive) in the data to end the search.
-
prefixFlags - A boolean array whose first three elements are used to store the state
- required to detect NAL units where the NAL unit prefix spans array boundaries. The array
- must be at least 3 elements long.
-
Returns:
-
The offset of the NAL unit, or endOffset if a NAL unit was not found.
public final class ParsableByteArray
-extends Object
-
Wraps a byte array, providing a set of methods for parsing data from it. Numerical values are
- parsed with the assumption that their constituent bytes are in big endian order.
Resets the position to zero and the limit to the specified value. If the limit exceeds the
- capacity, data is replaced with a new array of sufficient size.
- A line is considered to be terminated by any one of a carriage return ('\r'), a line feed
- ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The system's default
- charset (UTF-8) is used.
-
-
Returns:
-
A String containing the contents of the line, not including any line-termination
- characters, or null if the end of the stream has been reached.
- This class is provided for convenience, however it is expected that most applications will
- implement their own player controls and therefore not require this class.
public final class SlidingPercentile
-extends Object
-
Calculate any percentile over a sliding window of weighted values. A maximum total weight is
- configured. Once the maximum weight is reached, the oldest value is reduced in weight until it
- reaches zero and is removed. This maintains a constant total weight at steady state.
-
- SlidingPercentile can be used for bandwidth estimation based on a sliding window of past
- download rate observations. This is an alternative to sliding mean and exponential averaging
- which suffer from susceptibility to outliers and slow adaptation to step functions.
On platform API levels 19 and 20, okhttp's implementation of InputStream.close() can
- block for a long time if the stream has a lot of data remaining.
public static int binarySearchFloor(long[] a,
- long key,
- boolean inclusive,
- boolean stayInBounds)
-
Returns the index of the largest value in an array that is less than (or optionally equal to)
- a specified key.
-
- The search is performed using a binary search algorithm, and so the array must be sorted.
-
-
Parameters:
-
a - The array to search.
-
key - The key being searched for.
-
inclusive - If the key is present in the array, whether to return the corresponding index.
- If false then the returned index corresponds to the largest value in the array that is
- strictly less than the key.
-
stayInBounds - If true, then 0 will be returned in the case that the key is smaller than
- the smallest value in the array. If false then -1 will be returned.
public static int binarySearchCeil(long[] a,
- long key,
- boolean inclusive,
- boolean stayInBounds)
-
Returns the index of the smallest value in an array that is greater than (or optionally equal
- to) a specified key.
-
- The search is performed using a binary search algorithm, and so the array must be sorted.
-
-
Parameters:
-
a - The array to search.
-
key - The key being searched for.
-
inclusive - If the key is present in the array, whether to return the corresponding index.
- If false then the returned index corresponds to the smallest value in the array that is
- strictly greater than the key.
-
stayInBounds - If true, then (a.length - 1) will be returned in the case that the
- key is greater than the largest value in the array. If false then a.length will be
- returned.
public static <T> int binarySearchFloor(List<? extends Comparable<? super T>> list,
- T key,
- boolean inclusive,
- boolean stayInBounds)
-
Returns the index of the largest value in an list that is less than (or optionally equal to)
- a specified key.
-
- The search is performed using a binary search algorithm, and so the list must be sorted.
-
-
Parameters:
-
list - The list to search.
-
key - The key being searched for.
-
inclusive - If the key is present in the list, whether to return the corresponding index.
- If false then the returned index corresponds to the largest value in the list that is
- strictly less than the key.
-
stayInBounds - If true, then 0 will be returned in the case that the key is smaller than
- the smallest value in the list. If false then -1 will be returned.
public static <T> int binarySearchCeil(List<? extends Comparable<? super T>> list,
- T key,
- boolean inclusive,
- boolean stayInBounds)
-
Returns the index of the smallest value in an list that is greater than (or optionally equal
- to) a specified key.
-
- The search is performed using a binary search algorithm, and so the list must be sorted.
-
-
Parameters:
-
list - The list to search.
-
key - The key being searched for.
-
inclusive - If the key is present in the list, whether to return the corresponding index.
- If false then the returned index corresponds to the smallest value in the list that is
- strictly greater than the key.
-
stayInBounds - If true, then (list.size() - 1) will be returned in the case that
- the key is greater than the largest value in the list. If false then list.size()
- will be returned.
public static long scaleLargeTimestamp(long timestamp,
- long multiplier,
- long divisor)
-
Scales a large timestamp.
-
- Logically, scaling consists of a multiplication followed by a division. The actual operations
- performed are designed to minimize the probability of overflow.
public static void maybeTerminateInputStream(HttpURLConnection connection,
- long bytesRemaining)
-
On platform API levels 19 and 20, okhttp's implementation of InputStream.close() can
- block for a long time if the stream has a lot of data remaining. Call this method before
- closing the input stream to make a best effort to cause the input stream to encounter an
- unexpected end of input, working around this issue. On other platform API levels, the method
- does nothing.
-
-
Parameters:
-
connection - The connection whose InputStream should be terminated.
-
bytesRemaining - The number of bytes remaining to be read from the input stream if its
- length is known. C.LENGTH_UNBOUNDED otherwise.
public static String escapeFileName(String fileName)
-
Escapes a string so that it's safe for use as a file or directory name on at least FAT32
- filesystems. FAT32 is the most restrictive of all filesystems still commonly used today.
-
-
For simplicity, this only handles common characters known to be illegal on FAT32:
- <, >, :, ", /, \, |, ?, and *. % is also escaped since it is used as the escape
- character. Escaping is performed in a consistent way so that no collisions occur and
- unescapeFileName(String) can be used to retrieve the original file name.
-
-
Parameters:
-
fileName - File name to be escaped.
-
Returns:
-
An escaped file name which will be safe for use on at least FAT32 filesystems.
Flushes input/output buffers that have not been dequeued yet and returns ownership of any
- dequeued input buffer to the decoder. Flushes any pending output currently in the decoder. The
- caller is still responsible for releasing any dequeued output buffers.
Flushes input/output buffers that have not been dequeued yet and returns ownership of any
- dequeued input buffer to the decoder. Flushes any pending output currently in the decoder. The
- caller is still responsible for releasing any dequeued output buffers.
protected abstract E decode(I inputBuffer,
- O outputBuffer,
- boolean reset)
-
Decodes the inputBuffer and stores any decoded output in outputBuffer.
-
-
Parameters:
-
inputBuffer - The buffer to decode.
-
outputBuffer - The output buffer to store decoded data. The flag
- Buffer.FLAG_DECODE_ONLY will be set if the same flag is set on inputBuffer,
- but the decoder may set/unset the flag if required. If the flag is set after this method
- returns, any output should not be presented.
-
reset - True if the decoder must be reset before decoding.
-
Returns:
-
A decoder exception if an error occurred, or null if decoding was successful.
This API (Application Programming Interface) document has pages corresponding to the items in the navigation bar, described as follows.
-
-
-
-
-
Overview
-
The Overview page is the front page of this API document and provides a list of all packages with a summary for each. This page can also contain an overall description of the set of packages.
-
-
-
Package
-
Each package has a page that contains a list of its classes and interfaces, with a summary for each. This page can contain six categories:
-
-
Interfaces (italic)
-
Classes
-
Enums
-
Exceptions
-
Errors
-
Annotation Types
-
-
-
-
Class/Interface
-
Each class, interface, nested class and nested interface has its own separate page. Each of these pages has three sections consisting of a class/interface description, summary tables, and detailed member descriptions:
-
-
Class inheritance diagram
-
Direct Subclasses
-
All Known Subinterfaces
-
All Known Implementing Classes
-
Class/interface declaration
-
Class/interface description
-
-
-
Nested Class Summary
-
Field Summary
-
Constructor Summary
-
Method Summary
-
-
-
Field Detail
-
Constructor Detail
-
Method Detail
-
-
Each summary entry contains the first sentence from the detailed description for that item. The summary entries are alphabetical, while the detailed descriptions are in the order they appear in the source code. This preserves the logical groupings established by the programmer.
-
-
-
Annotation Type
-
Each annotation type has its own separate page with the following sections:
-
-
Annotation Type declaration
-
Annotation Type description
-
Required Element Summary
-
Optional Element Summary
-
Element Detail
-
-
-
-
Enum
-
Each enum has its own separate page with the following sections:
-
-
Enum declaration
-
Enum description
-
Enum Constant Summary
-
Enum Constant Detail
-
-
-
-
Tree (Class Hierarchy)
-
There is a Class Hierarchy page for all packages, plus a hierarchy for each package. Each hierarchy page contains a list of classes and a list of interfaces. The classes are organized by inheritance structure starting with java.lang.Object. The interfaces do not inherit from java.lang.Object.
-
-
When viewing the Overview page, clicking on "Tree" displays the hierarchy for all packages.
-
When viewing a particular package, class or interface page, clicking "Tree" displays the hierarchy for only that package.
-
-
-
-
Deprecated API
-
The Deprecated API page lists all of the API that have been deprecated. A deprecated API is not recommended for use, generally due to improvements, and a replacement API is usually given. Deprecated APIs may be removed in future implementations.
-
-
-
Index
-
The Index contains an alphabetic list of all classes, interfaces, constructors, methods, and fields.
-
-
-
Prev/Next
-
These links take you to the next or previous class, interface, package, or related page.
-
-
-
Frames/No Frames
-
These links show and hide the HTML frames. All pages are available with or without frames.
-
-
-
All Classes
-
The All Classes link shows all classes and interfaces except non-static nested types.
-
-
-
Serialized Form
-
Each serializable or externalizable class has a description of its serialization fields and methods. This information is of interest to re-implementors, not to developers using the API. While there is no link in the navigation bar, you can get to this information by going to any serialized class and clicking "Serialized Form" in the "See also" section of the class description.
Determines whether the existing MediaCodec should be reconfigured for a new format by
- sending codec specific initialization data at the start of the next input buffer.
A LoadControl implementation that allows loads to continue in a sequence that prevents
- any loader from getting too far ahead or behind any of the other loaders.
The duration in microseconds, or C.UNKNOWN_TIME_US if the duration is unknown, or
- C.MATCH_LONGEST_US if the duration should match the duration of the longest track whose
- duration is known.
The length of the trailing window for a live broadcast in microseconds, or
- C.UNKNOWN_TIME_US if the stream is not live or if the window length is unspecified.
Whether to enable a workaround for an issue where an audio effect does not keep its session
- active across releasing/initializing a new audio track, on platform API version < 21.
An adaptive evaluator for video formats, which attempts to select the best quality possible
- given the current network conditions and state of the buffer.
For formats that belong to an adaptive video track (either describing the track, or describing
- a specific format within it), this is the maximum height of the video in pixels that will be
- encountered in the stream.
For formats that belong to an adaptive video track (either describing the track, or describing
- a specific format within it), this is the maximum width of the video in pixels that will be
- encountered in the stream.
On platform API levels 19 and 20, okhttp's implementation of InputStream.close() can
- block for a long time if the stream has a lot of data remaining.
Invoked when the output stream ends, meaning that the last output buffer has been processed
- and the MediaCodec.BUFFER_FLAG_END_OF_STREAM flag has been propagated through the
- decoder.
A consumer of samples should call this method to register themselves and gain access to the
- source through the returned SampleSource.SampleSourceReader.
The clockwise rotation that should be applied to the video for it to be rendered in the correct
- orientation, or MediaFormat.NO_VALUE if unknown or not applicable.
Sets the bottom padding fraction to apply when Cue.line is Cue.DIMEN_UNSET,
- as a fraction of the view's remaining height after its top and bottom padding have been
- subtracted.
True if the cause (i.e. the Throwable returned by Throwable.getCause()) was only caught
- by a fail-safe at the top level of the player. False otherwise.
-
-
-
diff --git a/docs/doc/reference-v1/stylesheet.css b/docs/doc/reference-v1/stylesheet.css
deleted file mode 100644
index 98055b22d6..0000000000
--- a/docs/doc/reference-v1/stylesheet.css
+++ /dev/null
@@ -1,574 +0,0 @@
-/* Javadoc style sheet */
-/*
-Overall document style
-*/
-
-@import url('resources/fonts/dejavu.css');
-
-body {
- background-color:#ffffff;
- color:#353833;
- font-family:'DejaVu Sans', Arial, Helvetica, sans-serif;
- font-size:14px;
- margin:0;
-}
-a:link, a:visited {
- text-decoration:none;
- color:#4A6782;
-}
-a:hover, a:focus {
- text-decoration:none;
- color:#bb7a2a;
-}
-a:active {
- text-decoration:none;
- color:#4A6782;
-}
-a[name] {
- color:#353833;
-}
-a[name]:hover {
- text-decoration:none;
- color:#353833;
-}
-pre {
- font-family:'DejaVu Sans Mono', monospace;
- font-size:14px;
-}
-h1 {
- font-size:20px;
-}
-h2 {
- font-size:18px;
-}
-h3 {
- font-size:16px;
- font-style:italic;
-}
-h4 {
- font-size:13px;
-}
-h5 {
- font-size:12px;
-}
-h6 {
- font-size:11px;
-}
-ul {
- list-style-type:disc;
-}
-code, tt {
- font-family:'DejaVu Sans Mono', monospace;
- font-size:14px;
- padding-top:4px;
- margin-top:8px;
- line-height:1.4em;
-}
-dt code {
- font-family:'DejaVu Sans Mono', monospace;
- font-size:14px;
- padding-top:4px;
-}
-table tr td dt code {
- font-family:'DejaVu Sans Mono', monospace;
- font-size:14px;
- vertical-align:top;
- padding-top:4px;
-}
-sup {
- font-size:8px;
-}
-/*
-Document title and Copyright styles
-*/
-.clear {
- clear:both;
- height:0px;
- overflow:hidden;
-}
-.aboutLanguage {
- float:right;
- padding:0px 21px;
- font-size:11px;
- z-index:200;
- margin-top:-9px;
-}
-.legalCopy {
- margin-left:.5em;
-}
-.bar a, .bar a:link, .bar a:visited, .bar a:active {
- color:#FFFFFF;
- text-decoration:none;
-}
-.bar a:hover, .bar a:focus {
- color:#bb7a2a;
-}
-.tab {
- background-color:#0066FF;
- color:#ffffff;
- padding:8px;
- width:5em;
- font-weight:bold;
-}
-/*
-Navigation bar styles
-*/
-.bar {
- background-color:#4D7A97;
- color:#FFFFFF;
- padding:.8em .5em .4em .8em;
- height:auto;/*height:1.8em;*/
- font-size:11px;
- margin:0;
-}
-.topNav {
- background-color:#4D7A97;
- color:#FFFFFF;
- float:left;
- padding:0;
- width:100%;
- clear:right;
- height:2.8em;
- padding-top:10px;
- overflow:hidden;
- font-size:12px;
-}
-.bottomNav {
- margin-top:10px;
- background-color:#4D7A97;
- color:#FFFFFF;
- float:left;
- padding:0;
- width:100%;
- clear:right;
- height:2.8em;
- padding-top:10px;
- overflow:hidden;
- font-size:12px;
-}
-.subNav {
- background-color:#dee3e9;
- float:left;
- width:100%;
- overflow:hidden;
- font-size:12px;
-}
-.subNav div {
- clear:left;
- float:left;
- padding:0 0 5px 6px;
- text-transform:uppercase;
-}
-ul.navList, ul.subNavList {
- float:left;
- margin:0 25px 0 0;
- padding:0;
-}
-ul.navList li{
- list-style:none;
- float:left;
- padding: 5px 6px;
- text-transform:uppercase;
-}
-ul.subNavList li{
- list-style:none;
- float:left;
-}
-.topNav a:link, .topNav a:active, .topNav a:visited, .bottomNav a:link, .bottomNav a:active, .bottomNav a:visited {
- color:#FFFFFF;
- text-decoration:none;
- text-transform:uppercase;
-}
-.topNav a:hover, .bottomNav a:hover {
- text-decoration:none;
- color:#bb7a2a;
- text-transform:uppercase;
-}
-.navBarCell1Rev {
- background-color:#F8981D;
- color:#253441;
- margin: auto 5px;
-}
-.skipNav {
- position:absolute;
- top:auto;
- left:-9999px;
- overflow:hidden;
-}
-/*
-Page header and footer styles
-*/
-.header, .footer {
- clear:both;
- margin:0 20px;
- padding:5px 0 0 0;
-}
-.indexHeader {
- margin:10px;
- position:relative;
-}
-.indexHeader span{
- margin-right:15px;
-}
-.indexHeader h1 {
- font-size:13px;
-}
-.title {
- color:#2c4557;
- margin:10px 0;
-}
-.subTitle {
- margin:5px 0 0 0;
-}
-.header ul {
- margin:0 0 15px 0;
- padding:0;
-}
-.footer ul {
- margin:20px 0 5px 0;
-}
-.header ul li, .footer ul li {
- list-style:none;
- font-size:13px;
-}
-/*
-Heading styles
-*/
-div.details ul.blockList ul.blockList ul.blockList li.blockList h4, div.details ul.blockList ul.blockList ul.blockListLast li.blockList h4 {
- background-color:#dee3e9;
- border:1px solid #d0d9e0;
- margin:0 0 6px -8px;
- padding:7px 5px;
-}
-ul.blockList ul.blockList ul.blockList li.blockList h3 {
- background-color:#dee3e9;
- border:1px solid #d0d9e0;
- margin:0 0 6px -8px;
- padding:7px 5px;
-}
-ul.blockList ul.blockList li.blockList h3 {
- padding:0;
- margin:15px 0;
-}
-ul.blockList li.blockList h2 {
- padding:0px 0 20px 0;
-}
-/*
-Page layout container styles
-*/
-.contentContainer, .sourceContainer, .classUseContainer, .serializedFormContainer, .constantValuesContainer {
- clear:both;
- padding:10px 20px;
- position:relative;
-}
-.indexContainer {
- margin:10px;
- position:relative;
- font-size:12px;
-}
-.indexContainer h2 {
- font-size:13px;
- padding:0 0 3px 0;
-}
-.indexContainer ul {
- margin:0;
- padding:0;
-}
-.indexContainer ul li {
- list-style:none;
- padding-top:2px;
-}
-.contentContainer .description dl dt, .contentContainer .details dl dt, .serializedFormContainer dl dt {
- font-size:12px;
- font-weight:bold;
- margin:10px 0 0 0;
- color:#4E4E4E;
-}
-.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd {
- margin:5px 0 10px 0px;
- font-size:14px;
- font-family:'DejaVu Sans Mono',monospace;
-}
-.serializedFormContainer dl.nameValue dt {
- margin-left:1px;
- font-size:1.1em;
- display:inline;
- font-weight:bold;
-}
-.serializedFormContainer dl.nameValue dd {
- margin:0 0 0 1px;
- font-size:1.1em;
- display:inline;
-}
-/*
-List styles
-*/
-ul.horizontal li {
- display:inline;
- font-size:0.9em;
-}
-ul.inheritance {
- margin:0;
- padding:0;
-}
-ul.inheritance li {
- display:inline;
- list-style:none;
-}
-ul.inheritance li ul.inheritance {
- margin-left:15px;
- padding-left:15px;
- padding-top:1px;
-}
-ul.blockList, ul.blockListLast {
- margin:10px 0 10px 0;
- padding:0;
-}
-ul.blockList li.blockList, ul.blockListLast li.blockList {
- list-style:none;
- margin-bottom:15px;
- line-height:1.4;
-}
-ul.blockList ul.blockList li.blockList, ul.blockList ul.blockListLast li.blockList {
- padding:0px 20px 5px 10px;
- border:1px solid #ededed;
- background-color:#f8f8f8;
-}
-ul.blockList ul.blockList ul.blockList li.blockList, ul.blockList ul.blockList ul.blockListLast li.blockList {
- padding:0 0 5px 8px;
- background-color:#ffffff;
- border:none;
-}
-ul.blockList ul.blockList ul.blockList ul.blockList li.blockList {
- margin-left:0;
- padding-left:0;
- padding-bottom:15px;
- border:none;
-}
-ul.blockList ul.blockList ul.blockList ul.blockList li.blockListLast {
- list-style:none;
- border-bottom:none;
- padding-bottom:0;
-}
-table tr td dl, table tr td dl dt, table tr td dl dd {
- margin-top:0;
- margin-bottom:1px;
-}
-/*
-Table styles
-*/
-.overviewSummary, .memberSummary, .typeSummary, .useSummary, .constantsSummary, .deprecatedSummary {
- width:100%;
- border-left:1px solid #EEE;
- border-right:1px solid #EEE;
- border-bottom:1px solid #EEE;
-}
-.overviewSummary, .memberSummary {
- padding:0px;
-}
-.overviewSummary caption, .memberSummary caption, .typeSummary caption,
-.useSummary caption, .constantsSummary caption, .deprecatedSummary caption {
- position:relative;
- text-align:left;
- background-repeat:no-repeat;
- color:#253441;
- font-weight:bold;
- clear:none;
- overflow:hidden;
- padding:0px;
- padding-top:10px;
- padding-left:1px;
- margin:0px;
- white-space:pre;
-}
-.overviewSummary caption a:link, .memberSummary caption a:link, .typeSummary caption a:link,
-.useSummary caption a:link, .constantsSummary caption a:link, .deprecatedSummary caption a:link,
-.overviewSummary caption a:hover, .memberSummary caption a:hover, .typeSummary caption a:hover,
-.useSummary caption a:hover, .constantsSummary caption a:hover, .deprecatedSummary caption a:hover,
-.overviewSummary caption a:active, .memberSummary caption a:active, .typeSummary caption a:active,
-.useSummary caption a:active, .constantsSummary caption a:active, .deprecatedSummary caption a:active,
-.overviewSummary caption a:visited, .memberSummary caption a:visited, .typeSummary caption a:visited,
-.useSummary caption a:visited, .constantsSummary caption a:visited, .deprecatedSummary caption a:visited {
- color:#FFFFFF;
-}
-.overviewSummary caption span, .memberSummary caption span, .typeSummary caption span,
-.useSummary caption span, .constantsSummary caption span, .deprecatedSummary caption span {
- white-space:nowrap;
- padding-top:5px;
- padding-left:12px;
- padding-right:12px;
- padding-bottom:7px;
- display:inline-block;
- float:left;
- background-color:#F8981D;
- border: none;
- height:16px;
-}
-.memberSummary caption span.activeTableTab span {
- white-space:nowrap;
- padding-top:5px;
- padding-left:12px;
- padding-right:12px;
- margin-right:3px;
- display:inline-block;
- float:left;
- background-color:#F8981D;
- height:16px;
-}
-.memberSummary caption span.tableTab span {
- white-space:nowrap;
- padding-top:5px;
- padding-left:12px;
- padding-right:12px;
- margin-right:3px;
- display:inline-block;
- float:left;
- background-color:#4D7A97;
- height:16px;
-}
-.memberSummary caption span.tableTab, .memberSummary caption span.activeTableTab {
- padding-top:0px;
- padding-left:0px;
- padding-right:0px;
- background-image:none;
- float:none;
- display:inline;
-}
-.overviewSummary .tabEnd, .memberSummary .tabEnd, .typeSummary .tabEnd,
-.useSummary .tabEnd, .constantsSummary .tabEnd, .deprecatedSummary .tabEnd {
- display:none;
- width:5px;
- position:relative;
- float:left;
- background-color:#F8981D;
-}
-.memberSummary .activeTableTab .tabEnd {
- display:none;
- width:5px;
- margin-right:3px;
- position:relative;
- float:left;
- background-color:#F8981D;
-}
-.memberSummary .tableTab .tabEnd {
- display:none;
- width:5px;
- margin-right:3px;
- position:relative;
- background-color:#4D7A97;
- float:left;
-
-}
-.overviewSummary td, .memberSummary td, .typeSummary td,
-.useSummary td, .constantsSummary td, .deprecatedSummary td {
- text-align:left;
- padding:0px 0px 12px 10px;
-}
-th.colOne, th.colFirst, th.colLast, .useSummary th, .constantsSummary th,
-td.colOne, td.colFirst, td.colLast, .useSummary td, .constantsSummary td{
- vertical-align:top;
- padding-right:0px;
- padding-top:8px;
- padding-bottom:3px;
-}
-th.colFirst, th.colLast, th.colOne, .constantsSummary th {
- background:#dee3e9;
- text-align:left;
- padding:8px 3px 3px 7px;
-}
-td.colFirst, th.colFirst {
- white-space:nowrap;
- font-size:13px;
-}
-td.colLast, th.colLast {
- font-size:13px;
-}
-td.colOne, th.colOne {
- font-size:13px;
-}
-.overviewSummary td.colFirst, .overviewSummary th.colFirst,
-.useSummary td.colFirst, .useSummary th.colFirst,
-.overviewSummary td.colOne, .overviewSummary th.colOne,
-.memberSummary td.colFirst, .memberSummary th.colFirst,
-.memberSummary td.colOne, .memberSummary th.colOne,
-.typeSummary td.colFirst{
- width:25%;
- vertical-align:top;
-}
-td.colOne a:link, td.colOne a:active, td.colOne a:visited, td.colOne a:hover, td.colFirst a:link, td.colFirst a:active, td.colFirst a:visited, td.colFirst a:hover, td.colLast a:link, td.colLast a:active, td.colLast a:visited, td.colLast a:hover, .constantValuesContainer td a:link, .constantValuesContainer td a:active, .constantValuesContainer td a:visited, .constantValuesContainer td a:hover {
- font-weight:bold;
-}
-.tableSubHeadingColor {
- background-color:#EEEEFF;
-}
-.altColor {
- background-color:#FFFFFF;
-}
-.rowColor {
- background-color:#EEEEEF;
-}
-/*
-Content styles
-*/
-.description pre {
- margin-top:0;
-}
-.deprecatedContent {
- margin:0;
- padding:10px 0;
-}
-.docSummary {
- padding:0;
-}
-
-ul.blockList ul.blockList ul.blockList li.blockList h3 {
- font-style:normal;
-}
-
-div.block {
- font-size:14px;
- font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
-}
-
-td.colLast div {
- padding-top:0px;
-}
-
-
-td.colLast a {
- padding-bottom:3px;
-}
-/*
-Formatting effect styles
-*/
-.sourceLineNo {
- color:green;
- padding:0 30px 0 0;
-}
-h1.hidden {
- visibility:hidden;
- overflow:hidden;
- font-size:10px;
-}
-.block {
- display:block;
- margin:3px 10px 2px 0px;
- color:#474747;
-}
-.deprecatedLabel, .descfrmTypeLabel, .memberNameLabel, .memberNameLink,
-.overrideSpecifyLabel, .packageHierarchyLabel, .paramLabel, .returnLabel,
-.seeLabel, .simpleTagLabel, .throwsLabel, .typeNameLabel, .typeNameLink {
- font-weight:bold;
-}
-.deprecationComment, .emphasizedPhrase, .interfaceName {
- font-style:italic;
-}
-
-div.block div.block span.deprecationComment, div.block div.block span.emphasizedPhrase,
-div.block div.block span.interfaceName {
- font-style:normal;
-}
-
-div.contentContainer ul.blockList li.blockList h2{
- padding-bottom:0px;
-}
diff --git a/docs/drm.md b/docs/drm.md
index 181f5c2c7c..dab0edae89 100644
--- a/docs/drm.md
+++ b/docs/drm.md
@@ -26,12 +26,6 @@ outlined in the sections below.
To play streams with rotating keys, pass `true` to
`MediaItem.Builder.setDrmMultiSession` when building the media item.
-{% include known-issue-box.html issue-id="4133" description="There may be a
-slight pause in playback when key rotation occurs." %}
-
-{% include known-issue-box.html issue-id="3561" description="On API level 22
-and below, the output surface may flicker when key rotation occurs." %}
-
### Multi-key content ###
Multi-key content consists of multiple streams, where some streams use different
diff --git a/docs/hello-world.md b/docs/hello-world.md
index 048bb5b24d..01db16036c 100644
--- a/docs/hello-world.md
+++ b/docs/hello-world.md
@@ -70,6 +70,7 @@ modules individually.
* `exoplayer-core`: Core functionality (required).
* `exoplayer-dash`: Support for DASH content.
* `exoplayer-hls`: Support for HLS content.
+* `exoplayer-rtsp`: Support for RTSP content.
* `exoplayer-smoothstreaming`: Support for SmoothStreaming content.
* `exoplayer-transformer`: Media transformation functionality.
* `exoplayer-ui`: UI components and resources for use with ExoPlayer.
@@ -98,12 +99,9 @@ to prevent build errors.
## Creating the player ##
-You can create an `ExoPlayer` instance using `SimpleExoPlayer.Builder` or
-`ExoPlayer.Builder`. The builders provide a range of customization options for
-creating `ExoPlayer` instances. For the vast majority of use cases
-`SimpleExoPlayer.Builder` should be used. This builder returns
-`SimpleExoPlayer`, which extends `ExoPlayer` to add additional high level player
-functionality. The code below is an example of creating a `SimpleExoPlayer`.
+You can create an `ExoPlayer` instance using `SimpleExoPlayer.Builder`, which
+provides a range of customization options. The code below is the simplest
+example of creating an instance.
~~~
SimpleExoPlayer player = new SimpleExoPlayer.Builder(context).build();
@@ -132,7 +130,7 @@ shows you where). You can temporarily opt out from these exceptions being thrown
by calling `SimpleExoPlayer.setThrowsWhenUsingWrongThread(false)`, in which case
the issue will be logged as a warning instead. Using this opt out is not safe
and may result in unexpected or obscure errors. It will be removed in ExoPlayer
-2.14.
+2.16.
{:.info}
For more information about ExoPlayer's treading model, see the
@@ -224,9 +222,9 @@ on the player. Some of the most commonly used methods are listed below.
* `setShuffleModeEnabled` controls playlist shuffling.
* `setPlaybackParameters` adjusts playback speed and audio pitch.
-If the player is bound to a `PlayerView` or `PlayerControlView`, then user
-interaction with these components will cause corresponding methods on the player
-to be invoked.
+If the player is bound to a `StyledPlayerView` or `StyledPlayerControlView`,
+then user interaction with these components will cause corresponding methods on
+the player to be invoked.
## Releasing the player ##
diff --git a/docs/hls.md b/docs/hls.md
index 4bc432d947..9718306ac7 100644
--- a/docs/hls.md
+++ b/docs/hls.md
@@ -60,14 +60,14 @@ player.prepare();
You can retrieve the current manifest by calling `Player.getCurrentManifest`.
For HLS you should cast the returned object to `HlsManifest`. The
-`onTimelineChanged` callback of `Player.EventListener` is also called whenever
+`onTimelineChanged` callback of `Player.Listener` is also called whenever
the manifest is loaded. This will happen once for a on-demand content, and
possibly many times for live content. The code snippet below shows how an app
can do something whenever the manifest is loaded.
~~~
player.addListener(
- new Player.EventListener() {
+ new Player.Listener() {
@Override
public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) {
@@ -110,7 +110,7 @@ follow to improve your HLS content. Read our [Medium post about HLS playback in
ExoPlayer][] for a full explanation. The main points are:
* Use precise segment durations.
-* Use a continues media stream; avoid changes in the media structure across
+* Use a continuous media stream; avoid changes in the media structure across
segments.
* Use the `#EXT-X-INDEPENDENT-SEGMENTS` tag.
* Prefer demuxed streams, as opposed to files that include both video and audio.
diff --git a/docs/listening-to-player-events.md b/docs/listening-to-player-events.md
index 37e1954140..58dbb7fe75 100644
--- a/docs/listening-to-player-events.md
+++ b/docs/listening-to-player-events.md
@@ -5,16 +5,16 @@ title: Player events
## Listening to playback events ##
Events such as changes in state and playback errors are reported to registered
-[`Player.EventListener`][] instances. Registering a listener to receive such
+[`Player.Listener`][] instances. Registering a listener to receive such
events is easy:
~~~
// Add a listener to receive events from the player.
-player.addListener(eventListener);
+player.addListener(listener);
~~~
{: .language-java}
-`Player.EventListener` has empty default methods, so you only need to implement
+`Player.Listener` has empty default methods, so you only need to implement
the methods you're interested in. See the [Javadoc][] for a full description of
the methods and when they're called. Some of the most important methods are
described in more detail below.
@@ -28,7 +28,7 @@ should be preferred for different use cases.
Changes in player state can be received by implementing
`onPlaybackStateChanged(@State int state)` in a registered
-`Player.EventListener`. The player can be in one of four playback states:
+`Player.Listener`. The player can be in one of four playback states:
* `Player.STATE_IDLE`: This is the initial state, the state when the player is
stopped, and when playback failed.
@@ -68,7 +68,7 @@ public void onIsPlayingChanged(boolean isPlaying) {
Errors that cause playback to fail can be received by implementing
`onPlayerError(ExoPlaybackException error)` in a registered
-`Player.EventListener`. When a failure occurs, this method will be called
+`Player.Listener`. When a failure occurs, this method will be called
immediately before the playback state transitions to `Player.STATE_IDLE`.
Failed or stopped playbacks can be retried by calling `ExoPlayer.retry`.
@@ -107,7 +107,7 @@ public void onPlayerError(ExoPlaybackException error) {
Whenever the player changes to a new media item in the playlist
`onMediaItemTransition(MediaItem mediaItem,
@MediaItemTransitionReason int reason)` is called on registered
-`Player.EventListener`s. The reason indicates whether this was an automatic
+`Player.Listener`s. The reason indicates whether this was an automatic
transition, a seek (for example after calling `player.next()`), a repetition of
the same item, or caused by a playlist change (e.g., if the currently playing
item is removed).
@@ -115,7 +115,7 @@ item is removed).
### Seeking ###
Calling `Player.seekTo` methods results in a series of callbacks to registered
-`Player.EventListener` instances:
+`Player.Listener` instances:
1. `onPositionDiscontinuity` with `reason=DISCONTINUITY_REASON_SEEK`. This is
the direct result of calling `Player.seekTo`.
@@ -177,28 +177,12 @@ generic `onEvents` callback, for example to record media item change reasons
with `onMediaItemTransition`, but only act once all state changes can be used
together in `onEvents`.
-## Additional SimpleExoPlayer listeners ##
+## Using AnalyticsListener ##
-When using `SimpleExoPlayer`, additional listeners can be registered with the
-player.
-
-* `addAnalyticsListener`: Listen to detailed events that may be useful for
- analytics and logging purposes. Please refer to the [analytics page][] for
- more details.
-* `addTextOutput`: Listen to changes in the subtitle or caption cues.
-* `addMetadataOutput`: Listen to timed metadata events, such as timed ID3 and
- EMSG data.
-* `addVideoListener`: Listen to events related to video rendering that may be
- useful for adjusting the UI (e.g., the aspect ratio of the `Surface` onto
- which video is being rendered).
-* `addAudioListener`: Listen to events related to audio, such as when an audio
- session ID changes, and when the player volume is changed.
-* `addDeviceListener`: Listen to events related to the state of the device.
-
-ExoPlayer's UI components, such as `StyledPlayerView`, will register themselves
-as listeners to events that they are interested in. Hence manual registration
-using the methods above is only useful for applications that implement their own
-player UI, or need to listen to events for some other purpose.
+When using `SimpleExoPlayer`, an `AnalyticsListener` can be registered with the
+player by calling `addAnalyticsListener`. `AnalyticsListener` implementations
+are able to listen to detailed events that may be useful for analytics and
+logging purposes. Please refer to the [analytics page][] for more details.
### Using EventLogger ###
@@ -241,8 +225,8 @@ player
~~~
{: .language-java }
-[`Player.EventListener`]: {{ site.exo_sdk }}/Player.EventListener.html
-[Javadoc]: {{ site.exo_sdk }}/Player.EventListener.html
+[`Player.Listener`]: {{ site.exo_sdk }}/Player.Listener.html
+[Javadoc]: {{ site.exo_sdk }}/Player.Listener.html
[`Individual callbacks vs onEvents`]: #individual-callbacks-vs-onevents
[`ExoPlaybackException`]: {{ site.exo_sdk }}/ExoPlaybackException.html
[log output]: event-logger.html
diff --git a/docs/live-streaming.md b/docs/live-streaming.md
index 302e804137..86ead16d51 100644
--- a/docs/live-streaming.md
+++ b/docs/live-streaming.md
@@ -25,7 +25,7 @@ closer to the live edge again.
## Detecting and monitoring live playbacks ##
-Every time a live window is updated, registered `Player.EventListener` instances
+Every time a live window is updated, registered `Player.Listener` instances
will receive an `onTimelineChanged` event. You can retrieve details about the
current live playback by querying various `Player` and `Timeline.Window`
methods, as listed below and shown in the following figure.
@@ -135,7 +135,7 @@ setting `minPlaybackSpeed` and `maxPlaybackSpeed` to `1.0f`.
The playback position may fall behind the live window, for example if the player
is paused or buffering for a long enough period of time. If this happens then
playback will fail and a `BehindLiveWindowException` will be reported via
-`Player.EventListener.onPlayerError`. Application code may wish to handle such
+`Player.Listener.onPlayerError`. Application code may wish to handle such
errors by resuming playback at the default position. The [PlayerActivity][] of
the demo app exemplifies this approach.
diff --git a/docs/media-sources.md b/docs/media-sources.md
index f257dce5c5..3084cf8d17 100644
--- a/docs/media-sources.md
+++ b/docs/media-sources.md
@@ -15,6 +15,7 @@ instances of the following content `MediaSource` implementations:
* `SsMediaSource` for [SmoothStreaming][].
* `HlsMediaSource` for [HLS][].
* `ProgressiveMediaSource` for [regular media files][].
+* `RtspMediaSource` for [RTSP][].
`DefaultMediaSourceFactory` can also create more complex media sources depending
on the properties of the corresponding media items. This is described in more
diff --git a/docs/playlists.md b/docs/playlists.md
index a3ca0c68bd..bc06164d39 100644
--- a/docs/playlists.md
+++ b/docs/playlists.md
@@ -101,7 +101,7 @@ MediaItem mediaItem =
## Detecting when playback transitions to another media item ##
When playback transitions to another media item, or starts repeating the same
-media item, `EventListener.onMediaItemTransition(MediaItem,
+media item, `Listener.onMediaItemTransition(MediaItem,
@MediaItemTransitionReason)` is called. This callback receives the new media
item, along with a `@MediaItemTransitionReason` indicating why the transition
occurred. A common use case for `onMediaItemTransition` is to update the
@@ -135,7 +135,7 @@ public void onMediaItemTransition(
## Detecting when the playlist changes ##
When a media item is added, removed or moved,
-`EventListener.onTimelineChanged(Timeline, @TimelineChangeReason)` is called
+`Listener.onTimelineChanged(Timeline, @TimelineChangeReason)` is called
immediately with `TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED`. This callback is
called even when the player has not yet been prepared.
diff --git a/docs/retrieving-metadata.md b/docs/retrieving-metadata.md
new file mode 100644
index 0000000000..73ad82e614
--- /dev/null
+++ b/docs/retrieving-metadata.md
@@ -0,0 +1,90 @@
+---
+title: Retrieving metadata
+---
+
+## During playback ##
+
+The metadata of the media can be retrieved during playback in multiple ways. The
+most straightforward is to listen for the
+`Player.EventListener#onMediaMetadataChanged` event; this will provide a
+[`MediaMetadata`][] object for use, which has fields such as `title` and
+`albumArtist`. Alternatively, calling `Player#getMediaMetadata` returns the same
+object.
+
+~~~
+public void onMediaMetadataChanged(MediaMetadata mediaMetadata) {
+ if (mediaMetadata.title != null) {
+ handleTitle(mediaMetadata.title);
+ }
+}
+
+~~~
+{: .language-java}
+
+If an application needs access to specific [`Metadata.Entry`][] objects, then it
+should listen for `Player#onStaticMetadataChanged` (for static metadata from the
+`Format`s) and/or add a `MetadataOutput` (for dynamic metadata delivered during
+playback) to the player. The return values of these callbacks are used to
+populate the `MediaMetadata`.
+
+## Without playback ##
+
+If playback is not needed, it is more efficient to use the
+[`MetadataRetriever`][] to extract the metadata because it avoids having to
+create and prepare a player.
+
+~~~
+ListenableFuture trackGroupsFuture =
+ MetadataRetriever.retrieveMetadata(context, mediaItem);
+Futures.addCallback(
+ trackGroupsFuture,
+ new FutureCallback() {
+ @Override
+ public void onSuccess(TrackGroupArray trackGroups) {
+ handleMetadata(trackGroups);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ handleFailure(t);
+ }
+ },
+ executor);
+~~~
+{: .language-java}
+
+## Motion photos ##
+
+It is also possible to extract the metadata of a motion photo, containing the
+image and video offset and length for example. The supported formats are:
+
+* JPEG motion photos recorded by Google Pixel and Samsung camera apps. This
+ format is playable by ExoPlayer and the associated metadata can therefore be
+ retrieved with a player or using the `MetadataRetriever`.
+* HEIC motion photos recorded by Google Pixel and Samsung camera apps. This
+ format is currently not playable by ExoPlayer and the associated metadata
+ should therefore be retrieved using the `MetadataRetriever`.
+
+For motion photos, the `TrackGroupArray` obtained with the `MetadataRetriever`
+contains a `TrackGroup` with a single `Format` enclosing a
+[`MotionPhotoMetadata`][] metadata entry.
+
+~~~
+for (int i = 0; i < trackGroups.length; i++) {
+ TrackGroup trackGroup = trackGroups.get(i);
+ Metadata metadata = trackGroup.getFormat(0).metadata;
+ if (metadata != null && metadata.length() == 1) {
+ Metadata.Entry metadataEntry = metadata.get(0);
+ if (metadataEntry instanceof MotionPhotoMetadata) {
+ MotionPhotoMetadata motionPhotoMetadata = (MotionPhotoMetadata) metadataEntry;
+ handleMotionPhotoMetadata(motionPhotoMetadata);
+ }
+ }
+}
+~~~
+{: .language-java}
+
+[`MediaMetadata`]: {{ site.exo_sdk }}/MediaMetadata.html
+[`Metadata.Entry`][]: {{ site.exo_sdk}}/metadata/Metadata.Entry.html
+[`MetadataRetriever`]: {{ site.exo_sdk }}/MetadataRetriever.html
+[`MotionPhotoMetadata`]: {{ site.exo_sdk }}/metadata/mp4/MotionPhotoMetadata.html
diff --git a/docs/rtsp.md b/docs/rtsp.md
new file mode 100644
index 0000000000..17c11048d0
--- /dev/null
+++ b/docs/rtsp.md
@@ -0,0 +1,57 @@
+---
+title: RTSP
+---
+
+{% include_relative _page_fragments/supported-formats-rtsp.md %}
+
+## Using MediaItem ##
+
+To play an RTSP stream, you need to depend on the RTSP module.
+
+~~~
+implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.X.X'
+~~~
+{: .language-gradle}
+
+You can then create a `MediaItem` for an RTSP URI and pass it to the player.
+
+~~~
+// Create a player instance.
+SimpleExoPlayer player = new SimpleExoPlayer.Builder(context).build();
+// Set the media item to be played.
+player.setMediaItem(MediaItem.fromUri(rtspUri));
+// Prepare the player.
+player.prepare();
+~~~
+{: .language-java}
+
+
+## Using RtspMediaSource ##
+
+For more customization options, you can create an `RtspMediaSource` and pass it
+directly to the player instead of a `MediaItem`.
+
+~~~
+// Create an RTSP media source pointing to an RTSP uri.
+MediaSource mediaSource =
+ new RtspMediaSource.Factory()
+ .createMediaSource(MediaItem.fromUri(rtspUri));
+// Create a player instance.
+SimpleExoPlayer player = new SimpleExoPlayer.Builder(context).build();
+// Set the media source to be played.
+player.setMediaSource(mediaSource);
+// Prepare the player.
+player.prepare();
+~~~
+{: .language-java}
+
+## Using RTSP behind a NAT ##
+
+ExoPlayer uses UDP as the default protocol for RTP transport.
+
+When streaming RTSP behind a NAT layer, the NAT might not be able to forward the
+incoming RTP/UDP packets to the device. This occurs if the NAT lacks the
+necessary UDP port mapping. If ExoPlayer detects there have not been incoming
+RTP packets for a while and the playback has not started yet, ExoPlayer tears
+down the current RTSP playback session, and retries playback using RTP-over-RTSP
+(transmitting RTP packets using the TCP connection opened for RTSP).
diff --git a/docs/smoothstreaming.md b/docs/smoothstreaming.md
index 3e686e4ad7..fb6824cde4 100644
--- a/docs/smoothstreaming.md
+++ b/docs/smoothstreaming.md
@@ -59,14 +59,14 @@ player.prepare();
You can retrieve the current manifest by calling `Player.getCurrentManifest`.
For SmoothStreaming you should cast the returned object to `SsManifest`. The
-`onTimelineChanged` callback of `Player.EventListener` is also called whenever
+`onTimelineChanged` callback of `Player.Listener` is also called whenever
the manifest is loaded. This will happen once for a on-demand content, and
possibly many times for live content. The code snippet below shows how an app
can do something whenever the manifest is loaded.
~~~
player.addListener(
- new Player.EventListener() {
+ new Player.Listener() {
@Override
public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) {
diff --git a/docs/supported-formats.md b/docs/supported-formats.md
index 10282d78ed..8270866bcd 100644
--- a/docs/supported-formats.md
+++ b/docs/supported-formats.md
@@ -41,6 +41,10 @@ and HDR video playback.
{% include_relative _page_fragments/supported-formats-progressive.md %}
+## RTSP ##
+
+{% include_relative _page_fragments/supported-formats-rtsp.md %}
+
## Sample formats ##
By default ExoPlayer uses Android's platform decoders. Hence the supported
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index d0910ee04e..3c73e49b9b 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -14,7 +14,7 @@ redirect_from:
* [Why do some streams fail with HTTP response code 301 or 302?][]
* [Why do some streams fail with UnrecognizedInputFormatException?][]
* [Why doesn't setPlaybackParameters work properly on some devices?][]
-* [What do "Player is accessed on the wrong thread" warnings mean?][]
+* [What do "Player is accessed on the wrong thread" errors mean?][]
* [How can I fix "Unexpected status line: ICY 200 OK"?][]
* [How can I query whether the stream being played is a live stream?][]
* [How do I keep audio playing when my app is backgrounded?][]
@@ -204,17 +204,7 @@ releases you provide to end users should not be affected by this issue.
#### What do "Player is accessed on the wrong thread" errors mean? ####
-If you are seeing `IllegalStateException` being thrown with the message "Player
-is accessed on the wrong thread", then some code in your app is accessing a
-`SimpleExoPlayer` instance on the wrong thread (the exception's stack trace
-shows you where). ExoPlayer instances need to be accessed from a single thread
-only. In most cases, this should be the application's main thread. For details,
-please read through the ["Threading model" section of the ExoPlayer Javadoc][].
-You can temporarily opt out from these exceptions being thrown by calling
-`SimpleExoPlayer.setThrowsWhenUsingWrongThread(false)`, in which case the issue
-will be logged as a warning instead. Using this opt out is not safe and may
-result in unexpected or obscure errors. The opt out will be removed in ExoPlayer
-2.14.
+See [A note on threading][] on the getting started page.
#### How can I fix "Unexpected status line: ICY 200 OK"? ####
@@ -313,7 +303,7 @@ is the official way to play YouTube videos on Android.
[Why do some streams fail with HTTP response code 301 or 302?]: #why-do-some-streams-fail-with-http-response-code-301-or-302
[Why do some streams fail with UnrecognizedInputFormatException?]: #why-do-some-streams-fail-with-unrecognizedinputformatexception
[Why doesn't setPlaybackParameters work properly on some devices?]: #why-doesnt-setplaybackparameters-work-properly-on-some-devices
-[What do "Player is accessed on the wrong thread" warnings mean?]: #what-do-player-is-accessed-on-the-wrong-thread-warnings-mean
+[What do "Player is accessed on the wrong thread" errors mean?]: #what-do-player-is-accessed-on-the-wrong-thread-errors-mean
[How can I fix "Unexpected status line: ICY 200 OK"?]: #how-can-i-fix-unexpected-status-line-icy-200-ok
[How can I query whether the stream being played is a live stream?]: #how-can-i-query-whether-the-stream-being-played-is-a-live-stream
[How do I keep audio playing when my app is backgrounded?]: #how-do-i-keep-audio-playing-when-my-app-is-backgrounded
@@ -347,7 +337,7 @@ is the official way to play YouTube videos on Android.
[`WakeLock`]: {{ site.android_sdk }}/android/os/PowerManager.WakeLock.html
[`SimpleExoPlayer`]: {{ site.exo_sdk }}/SimpleExoPlayer.html
[`setWakeMode`]: {{ site.exo_sdk }}/SimpleExoPlayer.html#setWakeMode-int-
-["Threading model" section of the ExoPlayer Javadoc]: {{ site.exo_sdk }}/ExoPlayer.html
+[A note on threading]: {{ site.base_url }}/hello-world.html#a-note-on-threading
[OkHttp extension]: {{ site.release_v2 }}/extensions/okhttp
[CORS enabled]: https://www.w3.org/wiki/CORS_Enabled
[Cast framework]: {{ site.google_sdk }}/cast/docs/chrome_sender/advanced#cors_requirements
diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle
index d0cc501fcb..0efda30b93 100644
--- a/extensions/cast/build.gradle
+++ b/extensions/cast/build.gradle
@@ -14,10 +14,9 @@
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
- api 'com.google.android.gms:play-services-cast-framework:18.1.0'
+ api 'com.google.android.gms:play-services-cast-framework:19.0.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-common')
- implementation project(modulePrefix + 'library-ui')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
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 4f75ba9d6e..60ba2e4a36 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
@@ -15,21 +15,31 @@
*/
package com.google.android.exoplayer2.ext.cast;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.min;
import android.os.Looper;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.BasePlayer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.audio.AudioAttributes;
+import com.google.android.exoplayer2.device.DeviceInfo;
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.text.Cue;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Assertions;
@@ -37,6 +47,8 @@ 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.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoSize;
import com.google.android.gms.cast.CastStatusCodes;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
@@ -50,7 +62,7 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
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.Collections;
+import com.google.common.collect.ImmutableList;
import java.util.List;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@@ -73,6 +85,21 @@ public final class CastPlayer extends BasePlayer {
ExoPlayerLibraryInfo.registerModule("goog.exo.cast");
}
+ @VisibleForTesting
+ /* package */ static final Commands PERMANENT_AVAILABLE_COMMANDS =
+ new Commands.Builder()
+ .addAll(
+ COMMAND_PLAY_PAUSE,
+ COMMAND_PREPARE_STOP,
+ COMMAND_SEEK_TO_DEFAULT_POSITION,
+ COMMAND_SEEK_TO_MEDIA_ITEM,
+ COMMAND_SET_REPEAT_MODE,
+ COMMAND_GET_CURRENT_MEDIA_ITEM,
+ COMMAND_GET_MEDIA_ITEMS,
+ COMMAND_GET_MEDIA_ITEMS_METADATA,
+ COMMAND_CHANGE_MEDIA_ITEMS)
+ .build();
+
private static final String TAG = "CastPlayer";
private static final int RENDERER_COUNT = 3;
@@ -95,7 +122,7 @@ public final class CastPlayer extends BasePlayer {
private final SeekResultCallback seekResultCallback;
// Listeners and notification.
- private final ListenerSet listeners;
+ private final ListenerSet listeners;
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state.
@@ -105,12 +132,14 @@ public final class CastPlayer extends BasePlayer {
private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection;
+ private Commands availableCommands;
@Player.State private int playbackState;
private int currentWindowIndex;
private long lastReportedPositionMs;
private int pendingSeekCount;
private int pendingSeekWindowIndex;
private long pendingSeekPositionMs;
+ @Nullable private PositionInfo pendingMediaItemRemovalPosition;
/**
* Creates a new cast player that uses a {@link DefaultMediaItemConverter}.
@@ -138,15 +167,14 @@ public final class CastPlayer extends BasePlayer {
new ListenerSet<>(
Looper.getMainLooper(),
Clock.DEFAULT,
- Player.Events::new,
- (listener, eventFlags) -> listener.onEvents(/* player= */ this, eventFlags));
-
+ (listener, flags) -> listener.onEvents(/* player= */ this, new Events(flags)));
playWhenReady = new StateHolder<>(false);
repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
playbackState = STATE_IDLE;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
currentTrackGroups = TrackGroupArray.EMPTY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
+ availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build();
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
@@ -231,14 +259,13 @@ public final class CastPlayer extends BasePlayer {
public MediaQueueItem getItem(int periodId) {
MediaStatus mediaStatus = getMediaStatus();
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
- ? mediaStatus.getItemById(periodId) : null;
+ ? mediaStatus.getItemById(periodId)
+ : null;
}
// CastSession methods.
- /**
- * Returns whether a cast session is available.
- */
+ /** Returns whether a cast session is available. */
public boolean isCastSessionAvailable() {
return remoteMediaClient != null;
}
@@ -254,47 +281,28 @@ public final class CastPlayer extends BasePlayer {
// Player implementation.
- @Override
- @Nullable
- public AudioComponent getAudioComponent() {
- return null;
- }
-
- @Override
- @Nullable
- public VideoComponent getVideoComponent() {
- return null;
- }
-
- @Override
- @Nullable
- public TextComponent getTextComponent() {
- return null;
- }
-
- @Override
- @Nullable
- public MetadataComponent getMetadataComponent() {
- return null;
- }
-
- @Override
- @Nullable
- public DeviceComponent getDeviceComponent() {
- // TODO(b/151792305): Implement the component.
- return null;
- }
-
@Override
public Looper getApplicationLooper() {
return Looper.getMainLooper();
}
+ @Override
+ public void addListener(Listener listener) {
+ EventListener eventListener = listener;
+ addListener(eventListener);
+ }
+
@Override
public void addListener(EventListener listener) {
listeners.add(listener);
}
+ @Override
+ public void removeListener(Listener listener) {
+ EventListener eventListener = listener;
+ removeListener(eventListener);
+ }
+
@Override
public void removeListener(EventListener listener) {
listeners.remove(listener);
@@ -314,11 +322,6 @@ public final class CastPlayer extends BasePlayer {
toMediaQueueItems(mediaItems), startWindowIndex, startPositionMs, repeatMode.value);
}
- @Override
- public void addMediaItems(List mediaItems) {
- addMediaItemsInternal(toMediaQueueItems(mediaItems), MediaQueueItem.INVALID_ITEM_ID);
- }
-
@Override
public void addMediaItems(int index, List mediaItems) {
Assertions.checkArgument(index >= 0);
@@ -351,8 +354,8 @@ public final class CastPlayer extends BasePlayer {
@Override
public void removeMediaItems(int fromIndex, int toIndex) {
- Assertions.checkArgument(
- fromIndex >= 0 && toIndex >= fromIndex && toIndex <= currentTimeline.getWindowCount());
+ Assertions.checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
+ toIndex = min(toIndex, currentTimeline.getWindowCount());
if (fromIndex == toIndex) {
// Do nothing.
return;
@@ -365,8 +368,8 @@ public final class CastPlayer extends BasePlayer {
}
@Override
- public void clearMediaItems() {
- removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ currentTimeline.getWindowCount());
+ public Commands getAvailableCommands() {
+ return availableCommands;
}
@Override
@@ -386,13 +389,6 @@ public final class CastPlayer extends BasePlayer {
return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
}
- @Deprecated
- @Override
- @Nullable
- public ExoPlaybackException getPlaybackError() {
- return getPlayerError();
- }
-
@Override
@Nullable
public ExoPlaybackException getPlayerError() {
@@ -430,7 +426,7 @@ public final class CastPlayer extends BasePlayer {
return playWhenReady.value;
}
- // We still call EventListener#onSeekProcessed() for backwards compatibility with listeners that
+ // We still call Listener#onSeekProcessed() for backwards compatibility with listeners that
// don't implement onPositionDiscontinuity().
@SuppressWarnings("deprecation")
@Override
@@ -441,17 +437,34 @@ public final class CastPlayer extends BasePlayer {
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
if (mediaStatus != null) {
if (getCurrentWindowIndex() != windowIndex) {
- remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid,
- positionMs, null).setResultCallback(seekResultCallback);
+ remoteMediaClient
+ .queueJumpToItem(
+ (int) currentTimeline.getPeriod(windowIndex, period).uid, positionMs, null)
+ .setResultCallback(seekResultCallback);
} else {
remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback);
}
+ PositionInfo oldPosition = getCurrentPositionInfo();
pendingSeekCount++;
pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs;
+ PositionInfo newPosition = getCurrentPositionInfo();
listeners.queueEvent(
Player.EVENT_POSITION_DISCONTINUITY,
- listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
+ listener -> {
+ listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK);
+ listener.onPositionDiscontinuity(oldPosition, newPosition, DISCONTINUITY_REASON_SEEK);
+ });
+ if (oldPosition.windowIndex != newPosition.windowIndex) {
+ // TODO(internal b/182261884): queue `onMediaItemTransition` event when the media item is
+ // repeated.
+ MediaItem mediaItem = getCurrentTimeline().getWindow(windowIndex, window).mediaItem;
+ listeners.queueEvent(
+ Player.EVENT_MEDIA_ITEM_TRANSITION,
+ listener ->
+ listener.onMediaItemTransition(mediaItem, MEDIA_ITEM_TRANSITION_REASON_SEEK));
+ }
+ updateAvailableCommandsAndNotifyIfChanged();
} else if (pendingSeekCount == 0) {
listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed);
}
@@ -459,7 +472,7 @@ public final class CastPlayer extends BasePlayer {
}
@Override
- public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
// Unsupported by the RemoteMediaClient API. Do nothing.
}
@@ -484,26 +497,6 @@ public final class CastPlayer extends BasePlayer {
sessionManager.endCurrentSession(false);
}
- @Override
- public int getRendererCount() {
- // We assume there are three renderers: video, audio, and text.
- return RENDERER_COUNT;
- }
-
- @Override
- public int getRendererType(int index) {
- switch (index) {
- case RENDERER_INDEX_VIDEO:
- return C.TRACK_TYPE_VIDEO;
- case RENDERER_INDEX_AUDIO:
- return C.TRACK_TYPE_AUDIO;
- case RENDERER_INDEX_TEXT:
- return C.TRACK_TYPE_TEXT;
- default:
- throw new IndexOutOfBoundsException();
- }
- }
-
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient == null) {
@@ -530,7 +523,8 @@ public final class CastPlayer extends BasePlayer {
}
@Override
- @RepeatMode public int getRepeatMode() {
+ @RepeatMode
+ public int getRepeatMode() {
return repeatMode.value;
}
@@ -556,9 +550,15 @@ public final class CastPlayer extends BasePlayer {
}
@Override
- public List getCurrentStaticMetadata() {
+ public ImmutableList getCurrentStaticMetadata() {
// CastPlayer does not currently support metadata.
- return Collections.emptyList();
+ return ImmutableList.of();
+ }
+
+ @Override
+ public MediaMetadata getMediaMetadata() {
+ // CastPlayer does not currently support metadata.
+ return MediaMetadata.EMPTY;
}
@Override
@@ -636,13 +636,118 @@ public final class CastPlayer extends BasePlayer {
return getBufferedPosition();
}
+ /** This method is not supported and returns {@link AudioAttributes#DEFAULT}. */
+ @Override
+ public AudioAttributes getAudioAttributes() {
+ return AudioAttributes.DEFAULT;
+ }
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void setVolume(float audioVolume) {}
+
+ /** This method is not supported and returns 1. */
+ @Override
+ public float getVolume() {
+ return 1;
+ }
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void clearVideoSurface() {}
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void clearVideoSurface(@Nullable Surface surface) {}
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void setVideoSurface(@Nullable Surface surface) {}
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {}
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {}
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) {}
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) {}
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void setVideoTextureView(@Nullable TextureView textureView) {}
+ /** This method is not supported and does nothing. */
+ @Override
+ public void clearVideoTextureView(@Nullable TextureView textureView) {}
+
+ /** This method is not supported and returns {@link VideoSize#UNKNOWN}. */
+ @Override
+ public VideoSize getVideoSize() {
+ return VideoSize.UNKNOWN;
+ }
+
+ /** This method is not supported and returns an empty list. */
+ @Override
+ public ImmutableList getCurrentCues() {
+ return ImmutableList.of();
+ }
+
+ /** This method is not supported and always returns {@link DeviceInfo#UNKNOWN}. */
+ @Override
+ public DeviceInfo getDeviceInfo() {
+ return DeviceInfo.UNKNOWN;
+ }
+
+ /** This method is not supported and always returns {@code 0}. */
+ @Override
+ public int getDeviceVolume() {
+ return 0;
+ }
+
+ /** This method is not supported and always returns {@code false}. */
+ @Override
+ public boolean isDeviceMuted() {
+ return false;
+ }
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void setDeviceVolume(int volume) {}
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void increaseDeviceVolume() {}
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void decreaseDeviceVolume() {}
+
+ /** This method is not supported and does nothing. */
+ @Override
+ public void setDeviceMuted(boolean muted) {}
+
// Internal methods.
+ // Call deprecated callbacks.
+ @SuppressWarnings("deprecation")
private void updateInternalStateAndNotifyIfChanged() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return;
}
+ int oldWindowIndex = this.currentWindowIndex;
+ @Nullable
+ Object oldPeriodUid =
+ !getCurrentTimeline().isEmpty()
+ ? getCurrentTimeline().getPeriod(oldWindowIndex, period, /* setIds= */ true).uid
+ : null;
boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
@@ -651,20 +756,62 @@ public final class CastPlayer extends BasePlayer {
Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying));
}
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
- updateTimelineAndNotifyIfChanged();
-
- int currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline);
- if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
- this.currentWindowIndex = currentWindowIndex;
+ boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged();
+ Timeline currentTimeline = getCurrentTimeline();
+ currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline);
+ @Nullable
+ Object currentPeriodUid =
+ !currentTimeline.isEmpty()
+ ? currentTimeline.getPeriod(currentWindowIndex, period, /* setIds= */ true).uid
+ : null;
+ if (!playingPeriodChangedByTimelineChange
+ && !Util.areEqual(oldPeriodUid, currentPeriodUid)
+ && pendingSeekCount == 0) {
+ // Report discontinuity and media item auto transition.
+ currentTimeline.getPeriod(oldWindowIndex, period, /* setIds= */ true);
+ currentTimeline.getWindow(oldWindowIndex, window);
+ long windowDurationMs = window.getDurationMs();
+ PositionInfo oldPosition =
+ new PositionInfo(
+ window.uid,
+ period.windowIndex,
+ period.uid,
+ period.windowIndex,
+ /* positionMs= */ windowDurationMs,
+ /* contentPositionMs= */ windowDurationMs,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ currentTimeline.getPeriod(currentWindowIndex, period, /* setIds= */ true);
+ currentTimeline.getWindow(currentWindowIndex, window);
+ PositionInfo newPosition =
+ new PositionInfo(
+ window.uid,
+ period.windowIndex,
+ period.uid,
+ period.windowIndex,
+ /* positionMs= */ window.getDefaultPositionMs(),
+ /* contentPositionMs= */ window.getDefaultPositionMs(),
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
listeners.queueEvent(
Player.EVENT_POSITION_DISCONTINUITY,
- listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION));
+ listener -> {
+ listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AUTO_TRANSITION);
+ listener.onPositionDiscontinuity(
+ oldPosition, newPosition, DISCONTINUITY_REASON_AUTO_TRANSITION);
+ });
+ listeners.queueEvent(
+ Player.EVENT_MEDIA_ITEM_TRANSITION,
+ listener ->
+ listener.onMediaItemTransition(
+ getCurrentMediaItem(), MEDIA_ITEM_TRANSITION_REASON_AUTO));
}
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED,
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection));
}
+ updateAvailableCommandsAndNotifyIfChanged();
listeners.flushEvents();
}
@@ -700,16 +847,81 @@ public final class CastPlayer extends BasePlayer {
}
}
- private void updateTimelineAndNotifyIfChanged() {
+ /**
+ * Updates the timeline and notifies {@link Player.Listener event listeners} if required.
+ *
+ * @return Whether the timeline change has caused a change of the period currently being played.
+ */
+ @SuppressWarnings("deprecation") // Calling deprecated listener method.
+ private boolean updateTimelineAndNotifyIfChanged() {
+ Timeline oldTimeline = currentTimeline;
+ int oldWindowIndex = currentWindowIndex;
+ boolean playingPeriodChanged = false;
if (updateTimeline()) {
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
+ Timeline timeline = currentTimeline;
+ // Call onTimelineChanged.
listeners.queueEvent(
Player.EVENT_TIMELINE_CHANGED,
- listener ->
- listener.onTimelineChanged(
- currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
+ listener -> {
+ listener.onTimelineChanged(
+ timeline, /* manifest= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
+ listener.onTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
+ });
+
+ // Call onPositionDiscontinuity if required.
+ Timeline currentTimeline = getCurrentTimeline();
+ boolean playingPeriodRemoved = false;
+ if (!oldTimeline.isEmpty()) {
+ Object oldPeriodUid =
+ castNonNull(oldTimeline.getPeriod(oldWindowIndex, period, /* setIds= */ true).uid);
+ playingPeriodRemoved = currentTimeline.getIndexOfPeriod(oldPeriodUid) == C.INDEX_UNSET;
+ }
+ if (playingPeriodRemoved) {
+ PositionInfo oldPosition;
+ if (pendingMediaItemRemovalPosition != null) {
+ oldPosition = pendingMediaItemRemovalPosition;
+ pendingMediaItemRemovalPosition = null;
+ } else {
+ // If the media item has been removed by another client, we don't know the removal
+ // position. We use the current position as a fallback.
+ oldTimeline.getPeriod(oldWindowIndex, period, /* setIds= */ true);
+ oldTimeline.getWindow(period.windowIndex, window);
+ oldPosition =
+ new PositionInfo(
+ window.uid,
+ period.windowIndex,
+ period.uid,
+ period.windowIndex,
+ getCurrentPosition(),
+ getContentPosition(),
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ }
+ PositionInfo newPosition = getCurrentPositionInfo();
+ listeners.queueEvent(
+ Player.EVENT_POSITION_DISCONTINUITY,
+ listener -> {
+ listener.onPositionDiscontinuity(DISCONTINUITY_REASON_REMOVE);
+ listener.onPositionDiscontinuity(
+ oldPosition, newPosition, DISCONTINUITY_REASON_REMOVE);
+ });
+ }
+
+ // Call onMediaItemTransition if required.
+ playingPeriodChanged =
+ currentTimeline.isEmpty() != oldTimeline.isEmpty() || playingPeriodRemoved;
+ if (playingPeriodChanged) {
+ listeners.queueEvent(
+ Player.EVENT_MEDIA_ITEM_TRANSITION,
+ listener ->
+ listener.onMediaItemTransition(
+ getCurrentMediaItem(), MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
+ }
+ updateAvailableCommandsAndNotifyIfChanged();
}
+ return playingPeriodChanged;
}
/**
@@ -761,7 +973,8 @@ public final class CastPlayer extends BasePlayer {
long id = mediaTrack.getId();
int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
int rendererIndex = getRendererIndexForTrackType(trackType);
- if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET
+ if (isTrackActive(id, activeTrackIds)
+ && rendererIndex != C.INDEX_UNSET
&& trackSelections[rendererIndex] == null) {
trackSelections[rendererIndex] = new CastTrackSelection(trackGroups[i]);
}
@@ -778,6 +991,16 @@ public final class CastPlayer extends BasePlayer {
return false;
}
+ private void updateAvailableCommandsAndNotifyIfChanged() {
+ Commands previousAvailableCommands = availableCommands;
+ availableCommands = getAvailableCommands(PERMANENT_AVAILABLE_COMMANDS);
+ if (!availableCommands.equals(previousAvailableCommands)) {
+ listeners.queueEvent(
+ Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
+ listener -> listener.onAvailableCommandsChanged(availableCommands));
+ }
+ }
+
@Nullable
private PendingResult setMediaItemsInternal(
MediaQueueItem[] mediaQueueItems,
@@ -792,6 +1015,10 @@ public final class CastPlayer extends BasePlayer {
startWindowIndex = getCurrentWindowIndex();
startPositionMs = getCurrentPosition();
}
+ Timeline currentTimeline = getCurrentTimeline();
+ if (!currentTimeline.isEmpty()) {
+ pendingMediaItemRemovalPosition = getCurrentPositionInfo();
+ }
return remoteMediaClient.queueLoad(
mediaQueueItems,
min(startWindowIndex, mediaQueueItems.length - 1),
@@ -827,14 +1054,47 @@ public final class CastPlayer extends BasePlayer {
if (remoteMediaClient == null || getMediaStatus() == null) {
return null;
}
+ Timeline timeline = getCurrentTimeline();
+ if (!timeline.isEmpty()) {
+ Object periodUid =
+ castNonNull(timeline.getPeriod(getCurrentPeriodIndex(), period, /* setIds= */ true).uid);
+ for (int uid : uids) {
+ if (periodUid.equals(uid)) {
+ pendingMediaItemRemovalPosition = getCurrentPositionInfo();
+ break;
+ }
+ }
+ }
return remoteMediaClient.queueRemoveItems(uids, /* customData= */ null);
}
+ private PositionInfo getCurrentPositionInfo() {
+ Timeline currentTimeline = getCurrentTimeline();
+ @Nullable
+ Object newPeriodUid =
+ !currentTimeline.isEmpty()
+ ? currentTimeline.getPeriod(getCurrentPeriodIndex(), period, /* setIds= */ true).uid
+ : null;
+ @Nullable
+ Object newWindowUid =
+ newPeriodUid != null ? currentTimeline.getWindow(period.windowIndex, window).uid : null;
+ return new PositionInfo(
+ newWindowUid,
+ getCurrentWindowIndex(),
+ newPeriodUid,
+ getCurrentPeriodIndex(),
+ getCurrentPosition(),
+ getContentPosition(),
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ }
+
private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
if (this.repeatMode.value != repeatMode) {
this.repeatMode.value = repeatMode;
listeners.queueEvent(
Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
+ updateAvailableCommandsAndNotifyIfChanged();
}
}
@@ -914,8 +1174,8 @@ public final class CastPlayer extends BasePlayer {
}
/**
- * Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a
- * {@link Player.RepeatMode}.
+ * Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a {@link
+ * Player.RepeatMode}.
*/
@RepeatMode
private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) {
@@ -1019,6 +1279,7 @@ public final class CastPlayer extends BasePlayer {
@Override
public void onQueueStatusUpdated() {
updateTimelineAndNotifyIfChanged();
+ listeners.flushEvents();
}
@Override
@@ -1054,8 +1315,12 @@ public final class CastPlayer extends BasePlayer {
@Override
public void onSessionResumeFailed(CastSession castSession, int statusCode) {
- Log.e(TAG, "Session resume failed. Error code " + statusCode + ": "
- + CastUtils.getLogString(statusCode));
+ Log.e(
+ TAG,
+ "Session resume failed. Error code "
+ + statusCode
+ + ": "
+ + CastUtils.getLogString(statusCode));
}
@Override
@@ -1065,8 +1330,12 @@ public final class CastPlayer extends BasePlayer {
@Override
public void onSessionStartFailed(CastSession castSession, int statusCode) {
- Log.e(TAG, "Session start failed. Error code " + statusCode + ": "
- + CastUtils.getLogString(statusCode));
+ Log.e(
+ TAG,
+ "Session start failed. Error code "
+ + statusCode
+ + ": "
+ + CastUtils.getLogString(statusCode));
}
@Override
@@ -1078,20 +1347,20 @@ public final class CastPlayer extends BasePlayer {
public void onSessionResuming(CastSession castSession, String s) {
// Do nothing.
}
-
}
private final class SeekResultCallback implements ResultCallback {
- // We still call EventListener#onSeekProcessed() for backwards compatibility with listeners that
+ // We still call Listener#onSeekProcessed() for backwards compatibility with listeners that
// don't implement onPositionDiscontinuity().
@SuppressWarnings("deprecation")
@Override
public void onResult(MediaChannelResult result) {
int statusCode = result.getStatus().getStatusCode();
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
- Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
- + CastUtils.getLogString(statusCode));
+ Log.e(
+ TAG,
+ "Seek failed. Error code " + statusCode + ": " + CastUtils.getLogString(statusCode));
}
if (--pendingSeekCount == 0) {
currentWindowIndex = pendingSeekWindowIndex;
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
index 22fe86d9e4..9d5ea06b17 100644
--- 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
@@ -36,6 +36,11 @@ import com.google.android.exoplayer2.util.Assertions;
this.trackGroup = trackGroup;
}
+ @Override
+ public int getType() {
+ return TYPE_UNSET;
+ }
+
@Override
public TrackGroup getTrackGroup() {
return trackGroup;
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
index d6644e6bb3..69702ea286 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
@@ -22,9 +22,7 @@ import com.google.android.gms.cast.framework.SessionProvider;
import java.util.Collections;
import java.util.List;
-/**
- * A convenience {@link OptionsProvider} to target the default cast receiver app.
- */
+/** A convenience {@link OptionsProvider} to target the default cast receiver app. */
public final class DefaultCastOptionsProvider implements OptionsProvider {
/**
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 c72a1fb316..5fbabb807e 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
@@ -58,7 +58,7 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
}
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
if (item.mediaMetadata.title != null) {
- metadata.putString(MediaMetadata.KEY_TITLE, item.mediaMetadata.title);
+ metadata.putString(MediaMetadata.KEY_TITLE, item.mediaMetadata.title.toString());
}
MediaInfo mediaInfo =
new MediaInfo.Builder(item.playbackProperties.uri.toString())
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java
index 049bc89b72..0a687f50b4 100644
--- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java
@@ -15,16 +15,44 @@
*/
package com.google.android.exoplayer2.ext.cast;
+import static com.google.android.exoplayer2.Player.COMMAND_ADJUST_DEVICE_VOLUME;
+import static com.google.android.exoplayer2.Player.COMMAND_CHANGE_MEDIA_ITEMS;
+import static com.google.android.exoplayer2.Player.COMMAND_GET_AUDIO_ATTRIBUTES;
+import static com.google.android.exoplayer2.Player.COMMAND_GET_CURRENT_MEDIA_ITEM;
+import static com.google.android.exoplayer2.Player.COMMAND_GET_DEVICE_VOLUME;
+import static com.google.android.exoplayer2.Player.COMMAND_GET_MEDIA_ITEMS;
+import static com.google.android.exoplayer2.Player.COMMAND_GET_MEDIA_ITEMS_METADATA;
+import static com.google.android.exoplayer2.Player.COMMAND_GET_TEXT;
+import static com.google.android.exoplayer2.Player.COMMAND_GET_VOLUME;
+import static com.google.android.exoplayer2.Player.COMMAND_PLAY_PAUSE;
+import static com.google.android.exoplayer2.Player.COMMAND_PREPARE_STOP;
+import static com.google.android.exoplayer2.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
+import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
+import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
+import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
+import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
+import static com.google.android.exoplayer2.Player.COMMAND_SET_DEVICE_VOLUME;
+import static com.google.android.exoplayer2.Player.COMMAND_SET_REPEAT_MODE;
+import static com.google.android.exoplayer2.Player.COMMAND_SET_SHUFFLE_MODE;
+import static com.google.android.exoplayer2.Player.COMMAND_SET_SPEED_AND_PITCH;
+import static com.google.android.exoplayer2.Player.COMMAND_SET_VIDEO_SURFACE;
+import static com.google.android.exoplayer2.Player.COMMAND_SET_VOLUME;
+import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE;
+import static com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
+import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
@@ -42,7 +70,9 @@ import com.google.android.gms.cast.framework.media.MediaQueue;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
+import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
@@ -50,6 +80,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
@@ -58,17 +89,15 @@ import org.mockito.Mockito;
public class CastPlayerTest {
private CastPlayer castPlayer;
-
private RemoteMediaClient.Callback remoteMediaClientCallback;
@Mock private RemoteMediaClient mockRemoteMediaClient;
@Mock private MediaStatus mockMediaStatus;
- @Mock private MediaInfo mockMediaInfo;
@Mock private MediaQueue mockMediaQueue;
@Mock private CastContext mockCastContext;
@Mock private SessionManager mockSessionManager;
@Mock private CastSession mockCastSession;
- @Mock private Player.EventListener mockListener;
+ @Mock private Player.Listener mockListener;
@Mock private PendingResult mockPendingResult;
@Captor
@@ -76,8 +105,8 @@ public class CastPlayerTest {
setResultCallbackArgumentCaptor;
@Captor private ArgumentCaptor callbackArgumentCaptor;
-
@Captor private ArgumentCaptor queueItemsArgumentCaptor;
+ @Captor private ArgumentCaptor mediaItemCaptor;
@SuppressWarnings("deprecation")
@Before
@@ -119,7 +148,7 @@ public class CastPlayerTest {
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
setResultCallbackArgumentCaptor
.getValue()
- .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
+ .onResult(mock(RemoteMediaClient.MediaChannelResult.class));
verifyNoMoreInteractions(mockListener);
}
@@ -139,7 +168,7 @@ public class CastPlayerTest {
// Upon result, the remote media client is still paused. The state should reflect that.
setResultCallbackArgumentCaptor
.getValue()
- .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
+ .onResult(mock(RemoteMediaClient.MediaChannelResult.class));
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
verify(mockListener).onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
@@ -193,7 +222,7 @@ public class CastPlayerTest {
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
setResultCallbackArgumentCaptor
.getValue()
- .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
+ .onResult(mock(RemoteMediaClient.MediaChannelResult.class));
verifyNoMoreInteractions(mockListener);
}
@@ -214,7 +243,7 @@ public class CastPlayerTest {
// Upon result, the repeat mode is ALL. The state should reflect that.
setResultCallbackArgumentCaptor
.getValue()
- .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
+ .onResult(mock(RemoteMediaClient.MediaChannelResult.class));
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL);
}
@@ -268,6 +297,109 @@ public class CastPlayerTest {
assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2);
}
+ @SuppressWarnings("deprecation") // Verifies deprecated callback being called correctly.
+ @Test
+ public void setMediaItems_replaceExistingPlaylist_notifiesMediaItemTransition() {
+ List firstPlaylist = new ArrayList<>();
+ String uri1 = "http://www.google.com/video1";
+ String uri2 = "http://www.google.com/video2";
+ firstPlaylist.add(
+ new MediaItem.Builder().setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build());
+ firstPlaylist.add(
+ new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build());
+ ImmutableList secondPlaylist =
+ ImmutableList.of(
+ new MediaItem.Builder()
+ .setUri(Uri.EMPTY)
+ .setMimeType(MimeTypes.APPLICATION_MPD)
+ .build());
+
+ castPlayer.setMediaItems(
+ firstPlaylist, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L);
+ updateTimeLine(
+ firstPlaylist, /* mediaQueueItemIds= */ new int[] {1, 2}, /* currentItemId= */ 2);
+ // Replacing existing playlist.
+ castPlayer.setMediaItems(
+ secondPlaylist, /* startWindowIndex= */ 0, /* startPositionMs= */ 1000L);
+ updateTimeLine(secondPlaylist, /* mediaQueueItemIds= */ new int[] {3}, /* currentItemId= */ 3);
+
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder
+ .verify(mockListener, times(2))
+ .onMediaItemTransition(
+ mediaItemCaptor.capture(), eq(MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
+ inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
+ assertThat(mediaItemCaptor.getAllValues().get(1).playbackProperties.tag).isEqualTo(3);
+ }
+
+ @SuppressWarnings("deprecation") // Verifies deprecated callback being called correctly.
+ @Test
+ public void setMediaItems_replaceExistingPlaylist_notifiesPositionDiscontinuity() {
+ List firstPlaylist = new ArrayList<>();
+ String uri1 = "http://www.google.com/video1";
+ String uri2 = "http://www.google.com/video2";
+ firstPlaylist.add(
+ new MediaItem.Builder().setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build());
+ firstPlaylist.add(
+ new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build());
+ ImmutableList secondPlaylist =
+ ImmutableList.of(
+ new MediaItem.Builder()
+ .setUri(Uri.EMPTY)
+ .setMimeType(MimeTypes.APPLICATION_MPD)
+ .build());
+
+ castPlayer.setMediaItems(
+ firstPlaylist, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L);
+ updateTimeLine(
+ firstPlaylist,
+ /* mediaQueueItemIds= */ new int[] {1, 2},
+ /* currentItemId= */ 2,
+ /* streamTypes= */ new int[] {
+ MediaInfo.STREAM_TYPE_BUFFERED, MediaInfo.STREAM_TYPE_BUFFERED
+ },
+ /* durationsMs= */ new long[] {20_000, 20_000},
+ /* positionMs= */ 2000L);
+ // Replacing existing playlist.
+ castPlayer.setMediaItems(
+ secondPlaylist, /* startWindowIndex= */ 0, /* startPositionMs= */ 1000L);
+ updateTimeLine(
+ secondPlaylist,
+ /* mediaQueueItemIds= */ new int[] {3},
+ /* currentItemId= */ 3,
+ /* streamTypes= */ new int[] {MediaInfo.STREAM_TYPE_BUFFERED},
+ /* durationsMs= */ new long[] {20_000},
+ /* positionMs= */ 1000L);
+
+ Player.PositionInfo oldPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 2,
+ /* windowIndex= */ 1,
+ /* periodUid= */ 2,
+ /* periodIndex= */ 1,
+ /* positionMs= */ 2000,
+ /* contentPositionMs= */ 2000,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ Player.PositionInfo newPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 3,
+ /* windowIndex= */ 0,
+ /* periodUid= */ 3,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 1000,
+ /* contentPositionMs= */ 1000,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder.verify(mockListener).onPositionDiscontinuity(eq(DISCONTINUITY_REASON_REMOVE));
+ inOrder
+ .verify(mockListener)
+ .onPositionDiscontinuity(eq(oldPosition), eq(newPosition), eq(DISCONTINUITY_REASON_REMOVE));
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
+ }
+
@Test
public void addMediaItems_callsRemoteMediaClient() {
MediaItem.Builder builder = new MediaItem.Builder();
@@ -293,7 +425,7 @@ public class CastPlayerTest {
public void addMediaItems_insertAtIndex_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 2);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
String uri = "http://www.google.com/video3";
MediaItem anotherMediaItem =
new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_MPD).build();
@@ -316,7 +448,7 @@ public class CastPlayerTest {
public void moveMediaItem_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 2);
@@ -328,7 +460,7 @@ public class CastPlayerTest {
public void moveMediaItem_toBegin_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 0);
@@ -340,7 +472,7 @@ public class CastPlayerTest {
public void moveMediaItem_toEnd_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 4);
@@ -355,7 +487,7 @@ public class CastPlayerTest {
public void moveMediaItems_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 3, /* newIndex= */ 1);
@@ -368,7 +500,7 @@ public class CastPlayerTest {
public void moveMediaItems_toBeginning_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4, /* newIndex= */ 0);
@@ -381,7 +513,7 @@ public class CastPlayerTest {
public void moveMediaItems_toEnd_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 3);
@@ -396,7 +528,7 @@ public class CastPlayerTest {
public void moveMediaItems_noItems_doesNotCallRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 1, /* newIndex= */ 0);
@@ -407,7 +539,7 @@ public class CastPlayerTest {
public void moveMediaItems_noMove_doesNotCallRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 1);
@@ -418,7 +550,7 @@ public class CastPlayerTest {
public void removeMediaItems_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4);
@@ -429,7 +561,7 @@ public class CastPlayerTest {
public void clearMediaItems_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
castPlayer.clearMediaItems();
@@ -444,7 +576,7 @@ public class CastPlayerTest {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
List mediaItems = createMediaItems(mediaQueueItemIds);
- fillTimeline(mediaItems, mediaQueueItemIds);
+ addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
Timeline currentTimeline = castPlayer.getCurrentTimeline();
for (int i = 0; i < mediaItems.size(); i++) {
@@ -453,6 +585,899 @@ public class CastPlayerTest {
}
}
+ @Test
+ public void addMediaItems_notifiesMediaItemTransition() {
+ MediaItem mediaItem = createMediaItem(/* mediaQueueItemId= */ 1);
+ List mediaItems = ImmutableList.of(mediaItem);
+ int[] mediaQueueItemIds = new int[] {1};
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder
+ .verify(mockListener)
+ .onMediaItemTransition(
+ mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
+ inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
+ assertThat(mediaItemCaptor.getValue().playbackProperties.tag)
+ .isEqualTo(mediaItem.playbackProperties.tag);
+ }
+
+ @Test
+ public void clearMediaItems_notifiesMediaItemTransition() {
+ int[] mediaQueueItemIds = new int[] {1, 2};
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ castPlayer.clearMediaItems();
+ updateTimeLine(
+ /* mediaItems= */ ImmutableList.of(),
+ /* mediaQueueItemIds= */ new int[0],
+ /* currentItemId= */ C.INDEX_UNSET);
+
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder
+ .verify(mockListener)
+ .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
+ inOrder
+ .verify(mockListener)
+ .onMediaItemTransition(
+ /* mediaItem= */ null, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
+ inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer.
+ public void clearMediaItems_notifiesPositionDiscontinuity() {
+ int[] mediaQueueItemIds = new int[] {1, 2};
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(
+ mediaItems,
+ mediaQueueItemIds,
+ /* currentItemId= */ 1,
+ new int[] {MediaInfo.STREAM_TYPE_BUFFERED, MediaInfo.STREAM_TYPE_BUFFERED},
+ /* durationsMs= */ new long[] {20_000L, 30_000L},
+ /* positionMs= */ 1234);
+ castPlayer.clearMediaItems();
+ updateTimeLine(
+ /* mediaItems= */ ImmutableList.of(),
+ /* mediaQueueItemIds= */ new int[0],
+ /* currentItemId= */ C.INDEX_UNSET,
+ new int[] {MediaInfo.STREAM_TYPE_BUFFERED},
+ /* durationsMs= */ new long[] {20_000L},
+ /* positionMs= */ 0);
+
+ Player.PositionInfo oldPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 1,
+ /* windowIndex= */ 0,
+ /* periodUid= */ 1,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 1234,
+ /* contentPositionMs= */ 1234,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ Player.PositionInfo newPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ null,
+ /* windowIndex= */ 0,
+ /* periodUid= */ null,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 0,
+ /* contentPositionMs= */ 0,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder.verify(mockListener).onPositionDiscontinuity(eq(Player.DISCONTINUITY_REASON_REMOVE));
+ inOrder
+ .verify(mockListener)
+ .onPositionDiscontinuity(
+ eq(oldPosition), eq(newPosition), eq(Player.DISCONTINUITY_REASON_REMOVE));
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
+ }
+
+ @Test
+ public void removeCurrentMediaItem_notifiesMediaItemTransition() {
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ List mediaItems = ImmutableList.of(mediaItem1, mediaItem2);
+ int[] mediaQueueItemIds = new int[] {1, 2};
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ castPlayer.removeMediaItem(/* index= */ 0);
+ // Update with the new timeline after removal.
+ updateTimeLine(
+ ImmutableList.of(mediaItem2),
+ /* mediaQueueItemIds= */ new int[] {2},
+ /* currentItemId= */ 2);
+
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder
+ .verify(mockListener, times(2))
+ .onMediaItemTransition(
+ mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
+ inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
+ assertThat(mediaItemCaptor.getAllValues().get(0).playbackProperties.tag)
+ .isEqualTo(mediaItem1.playbackProperties.tag);
+ assertThat(mediaItemCaptor.getAllValues().get(1).playbackProperties.tag)
+ .isEqualTo(mediaItem2.playbackProperties.tag);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer.
+ public void removeCurrentMediaItem_notifiesPositionDiscontinuity() {
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ List mediaItems = ImmutableList.of(mediaItem1, mediaItem2);
+ int[] mediaQueueItemIds = new int[] {1, 2};
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(
+ mediaItems,
+ mediaQueueItemIds,
+ /* currentItemId= */ 1,
+ new int[] {MediaInfo.STREAM_TYPE_BUFFERED, MediaInfo.STREAM_TYPE_BUFFERED},
+ /* durationsMs= */ new long[] {20_000L, 30_000L},
+ /* positionMs= */ 1234);
+ castPlayer.removeMediaItem(/* index= */ 0);
+ // Update with the new timeline after removal.
+ updateTimeLine(
+ ImmutableList.of(mediaItem2),
+ /* mediaQueueItemIds= */ new int[] {2},
+ /* currentItemId= */ 2,
+ new int[] {MediaInfo.STREAM_TYPE_BUFFERED},
+ /* durationsMs= */ new long[] {20_000L},
+ /* positionMs= */ 0);
+
+ Player.PositionInfo oldPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 1,
+ /* windowIndex= */ 0,
+ /* periodUid= */ 1,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 1234,
+ /* contentPositionMs= */ 1234,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ Player.PositionInfo newPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 2,
+ /* windowIndex= */ 0,
+ /* periodUid= */ 2,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 0,
+ /* contentPositionMs= */ 0,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder.verify(mockListener).onPositionDiscontinuity(eq(Player.DISCONTINUITY_REASON_REMOVE));
+ inOrder
+ .verify(mockListener)
+ .onPositionDiscontinuity(
+ eq(oldPosition), eq(newPosition), eq(Player.DISCONTINUITY_REASON_REMOVE));
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
+ }
+
+ @Test
+ public void removeCurrentMediaItem_byRemoteClient_notifiesMediaItemTransition() {
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ List mediaItems = ImmutableList.of(mediaItem1, mediaItem2);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, new int[] {1, 2}, /* currentItemId= */ 1);
+ // Update with the new timeline after removal on the device.
+ updateTimeLine(
+ ImmutableList.of(mediaItem2),
+ /* mediaQueueItemIds= */ new int[] {2},
+ /* currentItemId= */ 2);
+
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder
+ .verify(mockListener, times(2))
+ .onMediaItemTransition(
+ mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
+ inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
+ List capturedMediaItems = mediaItemCaptor.getAllValues();
+ assertThat(capturedMediaItems.get(0).playbackProperties.tag)
+ .isEqualTo(mediaItem1.playbackProperties.tag);
+ assertThat(capturedMediaItems.get(1).playbackProperties.tag)
+ .isEqualTo(mediaItem2.playbackProperties.tag);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer.
+ public void removeCurrentMediaItem_byRemoteClient_notifiesPositionDiscontinuity() {
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ List mediaItems = ImmutableList.of(mediaItem1, mediaItem2);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(
+ mediaItems,
+ new int[] {1, 2},
+ /* currentItemId= */ 1,
+ new int[] {MediaInfo.STREAM_TYPE_BUFFERED, MediaInfo.STREAM_TYPE_BUFFERED},
+ /* durationsMs= */ new long[] {20_000L, 30_000L},
+ /* positionMs= */ 1234);
+ // Update with the new timeline after removal on the device.
+ updateTimeLine(
+ ImmutableList.of(mediaItem2),
+ /* mediaQueueItemIds= */ new int[] {2},
+ /* currentItemId= */ 2,
+ new int[] {MediaInfo.STREAM_TYPE_BUFFERED},
+ /* durationsMs= */ new long[] {30_000L},
+ /* positionMs= */ 0);
+
+ Player.PositionInfo oldPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 1,
+ /* windowIndex= */ 0,
+ /* periodUid= */ 1,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 0, // position at which we receive the timeline change
+ /* contentPositionMs= */ 0, // position at which we receive the timeline change
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ Player.PositionInfo newPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 2,
+ /* windowIndex= */ 0,
+ /* periodUid= */ 2,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 0,
+ /* contentPositionMs= */ 0,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder.verify(mockListener).onPositionDiscontinuity(eq(Player.DISCONTINUITY_REASON_REMOVE));
+ inOrder
+ .verify(mockListener)
+ .onPositionDiscontinuity(
+ eq(oldPosition), eq(newPosition), eq(Player.DISCONTINUITY_REASON_REMOVE));
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
+ }
+
+ @Test
+ public void removeNonCurrentMediaItem_doesNotNotifyMediaItemTransition() {
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ List mediaItems = ImmutableList.of(mediaItem1, mediaItem2);
+ int[] mediaQueueItemIds = new int[] {1, 2};
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ castPlayer.removeMediaItem(/* index= */ 1);
+ updateTimeLine(
+ ImmutableList.of(mediaItem1),
+ /* mediaQueueItemIds= */ new int[] {1},
+ /* currentItemId= */ 1);
+
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder
+ .verify(mockListener)
+ .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
+ inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer.
+ public void removeNonCurrentMediaItem_doesNotNotifyPositionDiscontinuity() {
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ List mediaItems = ImmutableList.of(mediaItem1, mediaItem2);
+ int[] mediaQueueItemIds = new int[] {1, 2};
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ castPlayer.removeMediaItem(/* index= */ 1);
+ updateTimeLine(
+ ImmutableList.of(mediaItem1),
+ /* mediaQueueItemIds= */ new int[] {1},
+ /* currentItemId= */ 1);
+
+ verify(mockListener, never()).onPositionDiscontinuity(anyInt());
+ verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
+ }
+
+ @Test
+ public void seekTo_otherWindow_notifiesMediaItemTransition() {
+ when(mockRemoteMediaClient.queueJumpToItem(anyInt(), anyLong(), eq(null)))
+ .thenReturn(mockPendingResult);
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ List mediaItems = ImmutableList.of(mediaItem1, mediaItem2);
+ int[] mediaQueueItemIds = new int[] {1, 2};
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1234);
+
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder
+ .verify(mockListener)
+ .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
+ inOrder
+ .verify(mockListener)
+ .onMediaItemTransition(
+ mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK));
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
+ assertThat(mediaItemCaptor.getValue().playbackProperties.tag)
+ .isEqualTo(mediaItem2.playbackProperties.tag);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer.
+ public void seekTo_otherWindow_notifiesPositionDiscontinuity() {
+ when(mockRemoteMediaClient.queueJumpToItem(anyInt(), anyLong(), eq(null)))
+ .thenReturn(mockPendingResult);
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ List mediaItems = ImmutableList.of(mediaItem1, mediaItem2);
+ int[] mediaQueueItemIds = new int[] {1, 2};
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1234);
+
+ Player.PositionInfo oldPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 1,
+ /* windowIndex= */ 0,
+ /* periodUid= */ 1,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 0,
+ /* contentPositionMs= */ 0,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ Player.PositionInfo newPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 2,
+ /* windowIndex= */ 1,
+ /* periodUid= */ 2,
+ /* periodIndex= */ 1,
+ /* positionMs= */ 1234,
+ /* contentPositionMs= */ 1234,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder.verify(mockListener).onPositionDiscontinuity(eq(Player.DISCONTINUITY_REASON_SEEK));
+ inOrder
+ .verify(mockListener)
+ .onPositionDiscontinuity(
+ eq(oldPosition), eq(newPosition), eq(Player.DISCONTINUITY_REASON_SEEK));
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer.
+ public void seekTo_sameWindow_doesNotNotifyMediaItemTransition() {
+ when(mockRemoteMediaClient.seek(anyLong())).thenReturn(mockPendingResult);
+ int[] mediaQueueItemIds = new int[] {1, 2};
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1234);
+
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder
+ .verify(mockListener)
+ .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
+ inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer.
+ public void seekTo_sameWindow_notifiesPositionDiscontinuity() {
+ when(mockRemoteMediaClient.seek(anyLong())).thenReturn(mockPendingResult);
+ int[] mediaQueueItemIds = new int[] {1, 2};
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1234);
+
+ Player.PositionInfo oldPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 1,
+ /* windowIndex= */ 0,
+ /* periodUid= */ 1,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 0,
+ /* contentPositionMs= */ 0,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ Player.PositionInfo newPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 1,
+ /* windowIndex= */ 0,
+ /* periodUid= */ 1,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 1234,
+ /* contentPositionMs= */ 1234,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder.verify(mockListener).onPositionDiscontinuity(eq(Player.DISCONTINUITY_REASON_SEEK));
+ inOrder
+ .verify(mockListener)
+ .onPositionDiscontinuity(
+ eq(oldPosition), eq(newPosition), eq(Player.DISCONTINUITY_REASON_SEEK));
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
+ }
+
+ @Test
+ public void autoTransition_notifiesMediaItemTransition() {
+ int[] mediaQueueItemIds = new int[] {1, 2};
+ // When the remote Cast player transitions to an item that wasn't played before, the media state
+ // delivers the duration for that media item which updates the timeline accordingly.
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 2);
+
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder
+ .verify(mockListener)
+ .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
+ inOrder
+ .verify(mockListener)
+ .onMediaItemTransition(
+ mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
+ inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt());
+ assertThat(mediaItemCaptor.getValue().playbackProperties.tag).isEqualTo(2);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer.
+ public void autoTransition_notifiesPositionDiscontinuity() {
+ int[] mediaQueueItemIds = new int[] {1, 2};
+ int[] streamTypes = {MediaInfo.STREAM_TYPE_BUFFERED, MediaInfo.STREAM_TYPE_BUFFERED};
+ long[] durationsFirstMs = {12500, C.TIME_UNSET};
+ // When the remote Cast player transitions to an item that wasn't played before, the media state
+ // delivers the duration for that media item which updates the timeline accordingly.
+ long[] durationsSecondMs = {12500, 22000};
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(
+ mediaItems,
+ mediaQueueItemIds,
+ /* currentItemId= */ 1,
+ /* streamTypes= */ streamTypes,
+ /* durationsMs= */ durationsFirstMs,
+ /* positionMs= */ C.TIME_UNSET);
+ updateTimeLine(
+ mediaItems,
+ mediaQueueItemIds,
+ /* currentItemId= */ 2,
+ /* streamTypes= */ streamTypes,
+ /* durationsMs= */ durationsSecondMs,
+ /* positionMs= */ C.TIME_UNSET);
+
+ Player.PositionInfo oldPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 1,
+ /* windowIndex= */ 0,
+ /* periodUid= */ 1,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 12500,
+ /* contentPositionMs= */ 12500,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ Player.PositionInfo newPosition =
+ new Player.PositionInfo(
+ /* windowUid= */ 2,
+ /* windowIndex= */ 1,
+ /* periodUid= */ 2,
+ /* periodIndex= */ 1,
+ /* positionMs= */ 0,
+ /* contentPositionMs= */ 0,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET);
+ InOrder inOrder = Mockito.inOrder(mockListener);
+ inOrder
+ .verify(mockListener)
+ .onPositionDiscontinuity(eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
+ inOrder
+ .verify(mockListener)
+ .onPositionDiscontinuity(
+ eq(oldPosition), eq(newPosition), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
+ inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
+ }
+
+ @Test
+ public void isCommandAvailable_isTrueForAvailableCommands() {
+ int[] mediaQueueItemIds = new int[] {1, 2};
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+
+ assertThat(castPlayer.isCommandAvailable(COMMAND_PLAY_PAUSE)).isTrue();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_PREPARE_STOP)).isTrue();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_DEFAULT_POSITION)).isTrue();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)).isTrue();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)).isTrue();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)).isFalse();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)).isTrue();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)).isFalse();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)).isFalse();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SET_REPEAT_MODE)).isTrue();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)).isTrue();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS)).isTrue();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)).isTrue();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isFalse();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_GET_VOLUME)).isFalse();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isFalse();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VOLUME)).isFalse();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SET_DEVICE_VOLUME)).isFalse();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isFalse();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isFalse();
+ assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TEXT)).isFalse();
+ }
+
+ @Test
+ public void isCommandAvailable_duringUnseekableItem_isFalseForSeekInCurrent() {
+ MediaItem mediaItem = createMediaItem(/* mediaQueueItemId= */ 1);
+ List mediaItems = ImmutableList.of(mediaItem);
+ int[] mediaQueueItemIds = new int[] {1};
+ int[] streamTypes = new int[] {MediaInfo.STREAM_TYPE_LIVE};
+ long[] durationsMs = new long[] {C.TIME_UNSET};
+
+ castPlayer.addMediaItem(mediaItem);
+ updateTimeLine(
+ mediaItems,
+ mediaQueueItemIds,
+ /* currentItemId= */ 1,
+ streamTypes,
+ durationsMs,
+ /* positionMs= */ C.TIME_UNSET);
+
+ assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)).isFalse();
+ }
+
+ @Test
+ public void seekTo_nextWindow_notifiesAvailableCommandsChanged() {
+ when(mockRemoteMediaClient.queueJumpToItem(anyInt(), anyLong(), eq(null)))
+ .thenReturn(mockPendingResult);
+ Player.Commands commandsWithSeekInCurrentAndToNext =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
+ Player.Commands commandsWithSeekInCurrentAndToPrevious =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
+ Player.Commands commandsWithSeekAnywhere =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
+ COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
+ COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
+ int[] mediaQueueItemIds = new int[] {1, 2, 3, 4};
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrentAndToNext);
+ // Check that there were no other calls to onAvailableCommandsChanged.
+ verify(mockListener).onAvailableCommandsChanged(any());
+
+ castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekAnywhere);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+
+ castPlayer.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+
+ castPlayer.seekTo(/* windowIndex= */ 3, /* positionMs= */ 0);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrentAndToPrevious);
+ verify(mockListener, times(3)).onAvailableCommandsChanged(any());
+ }
+
+ @Test
+ public void seekTo_previousWindow_notifiesAvailableCommandsChanged() {
+ when(mockRemoteMediaClient.queueJumpToItem(anyInt(), anyLong(), eq(null)))
+ .thenReturn(mockPendingResult);
+ Player.Commands commandsWithSeekInCurrentAndToNext =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
+ Player.Commands commandsWithSeekInCurrentAndToPrevious =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
+ Player.Commands commandsWithSeekAnywhere =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
+ COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
+ COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
+ int[] mediaQueueItemIds = new int[] {1, 2, 3, 4};
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 4);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrentAndToPrevious);
+ // Check that there were no other calls to onAvailableCommandsChanged.
+ verify(mockListener).onAvailableCommandsChanged(any());
+
+ castPlayer.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekAnywhere);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+
+ castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+
+ castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrentAndToNext);
+ verify(mockListener, times(3)).onAvailableCommandsChanged(any());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation") // Mocks deprecated method used by the CastPlayer.
+ public void seekTo_sameWindow_doesNotNotifyAvailableCommandsChanged() {
+ when(mockRemoteMediaClient.seek(anyLong())).thenReturn(mockPendingResult);
+ Player.Commands commandsWithSeekInCurrent =
+ createWithPermanentCommands(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
+ int[] mediaQueueItemIds = new int[] {1};
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrent);
+
+ castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 200);
+ castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 100);
+ // Check that there were no other calls to onAvailableCommandsChanged.
+ verify(mockListener).onAvailableCommandsChanged(any());
+ }
+
+ @Test
+ public void addMediaItem_atTheEnd_notifiesAvailableCommandsChanged() {
+ Player.Commands commandsWithSeekInCurrent =
+ createWithPermanentCommands(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
+ Player.Commands commandsWithSeekInCurrentAndToNext =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ MediaItem mediaItem3 = createMediaItem(/* mediaQueueItemId= */ 3);
+
+ castPlayer.addMediaItem(mediaItem1);
+ updateTimeLine(
+ ImmutableList.of(mediaItem1),
+ /* mediaQueueItemIds= */ new int[] {1},
+ /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrent);
+ // Check that there were no other calls to onAvailableCommandsChanged.
+ verify(mockListener).onAvailableCommandsChanged(any());
+
+ castPlayer.addMediaItem(mediaItem2);
+ updateTimeLine(
+ ImmutableList.of(mediaItem1, mediaItem2),
+ /* mediaQueueItemIds= */ new int[] {1, 2},
+ /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrentAndToNext);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+
+ castPlayer.addMediaItem(mediaItem3);
+ updateTimeLine(
+ ImmutableList.of(mediaItem1, mediaItem2, mediaItem3),
+ /* mediaQueueItemIds= */ new int[] {1, 2, 3},
+ /* currentItemId= */ 1);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+ }
+
+ @Test
+ public void addMediaItem_atTheStart_notifiesAvailableCommandsChanged() {
+ Player.Commands commandsWithSeekInCurrent =
+ createWithPermanentCommands(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
+ Player.Commands commandsWithSeekInCurrentAndToPrevious =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ MediaItem mediaItem3 = createMediaItem(/* mediaQueueItemId= */ 3);
+
+ castPlayer.addMediaItem(mediaItem1);
+ updateTimeLine(
+ ImmutableList.of(mediaItem1),
+ /* mediaQueueItemIds= */ new int[] {1},
+ /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrent);
+ // Check that there were no other calls to onAvailableCommandsChanged.
+ verify(mockListener).onAvailableCommandsChanged(any());
+
+ castPlayer.addMediaItem(/* index= */ 0, mediaItem2);
+ updateTimeLine(
+ ImmutableList.of(mediaItem2, mediaItem1),
+ /* mediaQueueItemIds= */ new int[] {2, 1},
+ /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrentAndToPrevious);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+
+ castPlayer.addMediaItem(/* index= */ 0, mediaItem3);
+ updateTimeLine(
+ ImmutableList.of(mediaItem3, mediaItem2, mediaItem1),
+ /* mediaQueueItemIds= */ new int[] {3, 2, 1},
+ /* currentItemId= */ 1);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+ }
+
+ @Test
+ public void removeMediaItem_atTheEnd_notifiesAvailableCommandsChanged() {
+ Player.Commands commandsWithoutSeek = createWithPermanentCommands();
+ Player.Commands commandsWithSeekInCurrent =
+ createWithPermanentCommands(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
+ Player.Commands commandsWithSeekInCurrentAndToNext =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ MediaItem mediaItem3 = createMediaItem(/* mediaQueueItemId= */ 3);
+
+ castPlayer.addMediaItems(ImmutableList.of(mediaItem1, mediaItem2, mediaItem3));
+ updateTimeLine(
+ ImmutableList.of(mediaItem1, mediaItem2, mediaItem3),
+ /* mediaQueueItemIds= */ new int[] {1, 2, 3},
+ /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrentAndToNext);
+ // Check that there were no other calls to onAvailableCommandsChanged.
+ verify(mockListener).onAvailableCommandsChanged(any());
+
+ castPlayer.removeMediaItem(/* index= */ 2);
+ updateTimeLine(
+ ImmutableList.of(mediaItem1, mediaItem2),
+ /* mediaQueueItemIds= */ new int[] {1, 2},
+ /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(any());
+
+ castPlayer.removeMediaItem(/* index= */ 1);
+ updateTimeLine(
+ ImmutableList.of(mediaItem1),
+ /* mediaQueueItemIds= */ new int[] {1},
+ /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrent);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+
+ castPlayer.removeMediaItem(/* index= */ 0);
+ updateTimeLine(
+ ImmutableList.of(),
+ /* mediaQueueItemIds= */ new int[0],
+ /* currentItemId= */ C.INDEX_UNSET);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithoutSeek);
+ verify(mockListener, times(3)).onAvailableCommandsChanged(any());
+ }
+
+ @Test
+ public void removeMediaItem_atTheStart_notifiesAvailableCommandsChanged() {
+ when(mockRemoteMediaClient.queueJumpToItem(anyInt(), anyLong(), eq(null)))
+ .thenReturn(mockPendingResult);
+ Player.Commands commandsWithoutSeek = createWithPermanentCommands();
+ Player.Commands commandsWithSeekInCurrent =
+ createWithPermanentCommands(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
+ Player.Commands commandsWithSeekInCurrentAndToPrevious =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+ MediaItem mediaItem3 = createMediaItem(/* mediaQueueItemId= */ 3);
+
+ castPlayer.addMediaItems(ImmutableList.of(mediaItem1, mediaItem2, mediaItem3));
+ updateTimeLine(
+ ImmutableList.of(mediaItem1, mediaItem2, mediaItem3),
+ /* mediaQueueItemIds= */ new int[] {1, 2, 3},
+ /* currentItemId= */ 3);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrentAndToPrevious);
+ // Check that there were no other calls to onAvailableCommandsChanged.
+ verify(mockListener).onAvailableCommandsChanged(any());
+
+ castPlayer.removeMediaItem(/* index= */ 0);
+ updateTimeLine(
+ ImmutableList.of(mediaItem2, mediaItem3),
+ /* mediaQueueItemIds= */ new int[] {2, 3},
+ /* currentItemId= */ 3);
+ verify(mockListener).onAvailableCommandsChanged(any());
+
+ castPlayer.removeMediaItem(/* index= */ 0);
+ updateTimeLine(
+ ImmutableList.of(mediaItem3),
+ /* mediaQueueItemIds= */ new int[] {3},
+ /* currentItemId= */ 3);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrent);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+
+ castPlayer.removeMediaItem(/* index= */ 0);
+ updateTimeLine(
+ ImmutableList.of(),
+ /* mediaQueueItemIds= */ new int[0],
+ /* currentItemId= */ C.INDEX_UNSET);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithoutSeek);
+ verify(mockListener, times(3)).onAvailableCommandsChanged(any());
+ }
+
+ @Test
+ public void removeMediaItem_current_notifiesAvailableCommandsChanged() {
+ Player.Commands commandsWithSeekInCurrent =
+ createWithPermanentCommands(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
+ Player.Commands commandsWithSeekInCurrentAndToNext =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
+ MediaItem mediaItem1 = createMediaItem(/* mediaQueueItemId= */ 1);
+ MediaItem mediaItem2 = createMediaItem(/* mediaQueueItemId= */ 2);
+
+ castPlayer.addMediaItems(ImmutableList.of(mediaItem1, mediaItem2));
+ updateTimeLine(
+ ImmutableList.of(mediaItem1, mediaItem2),
+ /* mediaQueueItemIds= */ new int[] {1, 2},
+ /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrentAndToNext);
+ // Check that there were no other calls to onAvailableCommandsChanged.
+ verify(mockListener).onAvailableCommandsChanged(any());
+
+ castPlayer.removeMediaItem(/* index= */ 0);
+ updateTimeLine(
+ ImmutableList.of(mediaItem2),
+ /* mediaQueueItemIds= */ new int[] {2},
+ /* currentItemId= */ 2);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrent);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+ }
+
+ @Test
+ public void setRepeatMode_all_notifiesAvailableCommandsChanged() {
+ when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), eq(null)))
+ .thenReturn(mockPendingResult);
+ Player.Commands commandsWithSeekInCurrent =
+ createWithPermanentCommands(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
+ Player.Commands commandsWithSeekAnywhere =
+ createWithPermanentCommands(
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
+ COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
+ COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
+ int[] mediaQueueItemIds = new int[] {1};
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrent);
+ // Check that there were no other calls to onAvailableCommandsChanged.
+ verify(mockListener).onAvailableCommandsChanged(any());
+
+ castPlayer.setRepeatMode(Player.REPEAT_MODE_ALL);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekAnywhere);
+ verify(mockListener, times(2)).onAvailableCommandsChanged(any());
+ }
+
+ @Test
+ public void setRepeatMode_one_doesNotNotifyAvailableCommandsChanged() {
+ when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), eq(null)))
+ .thenReturn(mockPendingResult);
+ Player.Commands commandsWithSeekInCurrent =
+ createWithPermanentCommands(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
+ int[] mediaQueueItemIds = new int[] {1};
+ List mediaItems = createMediaItems(mediaQueueItemIds);
+
+ castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrent);
+ // Check that there were no other calls to onAvailableCommandsChanged.
+ verify(mockListener).onAvailableCommandsChanged(any());
+
+ castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
+ verify(mockListener).onAvailableCommandsChanged(any());
+ }
+
private int[] createMediaQueueItemIds(int numberOfIds) {
int[] mediaQueueItemIds = new int[numberOfIds];
for (int i = 0; i < numberOfIds; i++) {
@@ -462,37 +1487,89 @@ public class CastPlayerTest {
}
private List createMediaItems(int[] mediaQueueItemIds) {
- MediaItem.Builder builder = new MediaItem.Builder();
List mediaItems = new ArrayList<>();
for (int mediaQueueItemId : mediaQueueItemIds) {
- MediaItem mediaItem =
- builder
- .setUri("http://www.google.com/video" + mediaQueueItemId)
- .setMimeType(MimeTypes.APPLICATION_MPD)
- .setTag(mediaQueueItemId)
- .build();
- mediaItems.add(mediaItem);
+ mediaItems.add(createMediaItem(mediaQueueItemId));
}
return mediaItems;
}
- private void fillTimeline(List mediaItems, int[] mediaQueueItemIds) {
+ private MediaItem createMediaItem(int mediaQueueItemId) {
+ return new MediaItem.Builder()
+ .setUri("http://www.google.com/video" + mediaQueueItemId)
+ .setMimeType(MimeTypes.APPLICATION_MPD)
+ .setTag(mediaQueueItemId)
+ .build();
+ }
+
+ private void addMediaItemsAndUpdateTimeline(List mediaItems, int[] mediaQueueItemIds) {
Assertions.checkState(mediaItems.size() == mediaQueueItemIds.length);
- List queueItems = new ArrayList<>();
- DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
- for (MediaItem mediaItem : mediaItems) {
- queueItems.add(converter.toMediaQueueItem(mediaItem));
- }
-
- // Set up mocks to allow the player to update the timeline.
- when(mockMediaQueue.getItemIds()).thenReturn(mediaQueueItemIds);
- when(mockMediaStatus.getCurrentItemId()).thenReturn(1);
- when(mockMediaStatus.getMediaInfo()).thenReturn(mockMediaInfo);
- when(mockMediaInfo.getStreamType()).thenReturn(MediaInfo.STREAM_TYPE_NONE);
- when(mockMediaStatus.getQueueItems()).thenReturn(queueItems);
-
castPlayer.addMediaItems(mediaItems);
+ updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1);
+ }
+
+ private void updateTimeLine(
+ List mediaItems, int[] mediaQueueItemIds, int currentItemId) {
+ int[] streamTypes = new int[mediaItems.size()];
+ Arrays.fill(streamTypes, MediaInfo.STREAM_TYPE_BUFFERED);
+ long[] durationsMs = new long[mediaItems.size()];
+ updateTimeLine(
+ mediaItems,
+ mediaQueueItemIds,
+ currentItemId,
+ streamTypes,
+ durationsMs,
+ /* positionMs= */ C.TIME_UNSET);
+ }
+
+ private void updateTimeLine(
+ List mediaItems,
+ int[] mediaQueueItemIds,
+ int currentItemId,
+ int[] streamTypes,
+ long[] durationsMs,
+ long positionMs) {
+ // Set up mocks to allow the player to update the timeline.
+ List queueItems = new ArrayList<>();
+ for (int i = 0; i < mediaQueueItemIds.length; i++) {
+ MediaItem mediaItem = mediaItems.get(i);
+ int mediaQueueItemId = mediaQueueItemIds[i];
+ int streamType = streamTypes[i];
+ long durationMs = durationsMs[i];
+ MediaInfo.Builder mediaInfoBuilder =
+ new MediaInfo.Builder(mediaItem.playbackProperties.uri.toString())
+ .setStreamType(streamType)
+ .setContentType(mediaItem.playbackProperties.mimeType);
+ if (durationMs != C.TIME_UNSET) {
+ mediaInfoBuilder.setStreamDuration(durationMs);
+ }
+ MediaInfo mediaInfo = mediaInfoBuilder.build();
+ MediaQueueItem mediaQueueItem = mock(MediaQueueItem.class);
+ when(mediaQueueItem.getItemId()).thenReturn(mediaQueueItemId);
+ when(mediaQueueItem.getMedia()).thenReturn(mediaInfo);
+ queueItems.add(mediaQueueItem);
+ if (mediaQueueItemId == currentItemId) {
+ when(mockRemoteMediaClient.getCurrentItem()).thenReturn(mediaQueueItem);
+ when(mockMediaStatus.getMediaInfo()).thenReturn(mediaInfo);
+ }
+ }
+ if (positionMs != C.TIME_UNSET) {
+ when(mockRemoteMediaClient.getApproximateStreamPosition()).thenReturn(positionMs);
+ }
+ when(mockMediaQueue.getItemIds()).thenReturn(mediaQueueItemIds);
+ when(mockMediaStatus.getQueueItems()).thenReturn(queueItems);
+ when(mockMediaStatus.getCurrentItemId())
+ .thenReturn(currentItemId == C.INDEX_UNSET ? 0 : currentItemId);
+
// Call listener to update the timeline of the player.
- remoteMediaClientCallback.onQueueStatusUpdated();
+ remoteMediaClientCallback.onStatusUpdated();
+ }
+
+ private static Player.Commands createWithPermanentCommands(
+ @Player.Command int... additionalCommands) {
+ Player.Commands.Builder builder = new Player.Commands.Builder();
+ builder.addAll(CastPlayer.PERMANENT_AVAILABLE_COMMANDS);
+ builder.addAll(additionalCommands);
+ return builder.build();
}
}
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java
index 9d65bada16..9d80725c56 100644
--- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java
@@ -51,7 +51,7 @@ public class DefaultMediaItemConverterTest {
MediaItem item =
builder
.setUri(Uri.parse("http://example.com"))
- .setMediaMetadata(new MediaMetadata.Builder().build())
+ .setMediaMetadata(MediaMetadata.EMPTY)
.setMimeType(MimeTypes.APPLICATION_MPD)
.setDrmUuid(C.WIDEVINE_UUID)
.setDrmLicenseUri("http://license.com")
diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle
index f50304fb94..e12f2f050a 100644
--- a/extensions/cronet/build.gradle
+++ b/extensions/cronet/build.gradle
@@ -28,6 +28,7 @@ dependencies {
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
+ androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
// Instrumentation tests assume that an app-packaged version of cronet is
// available.
androidTestImplementation 'org.chromium.net:cronet-embedded:72.3626.96'
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 2726b00c73..9b73387fc1 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
@@ -15,8 +15,8 @@
*/
package com.google.android.exoplayer2.ext.cronet;
+import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader;
import static com.google.android.exoplayer2.util.Util.castNonNull;
-import static java.lang.Math.max;
import android.net.Uri;
import android.text.TextUtils;
@@ -29,14 +29,16 @@ 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.HttpUtil;
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.Ascii;
import com.google.common.base.Predicate;
-import com.google.common.primitives.Ints;
+import com.google.common.net.HttpHeaders;
+import com.google.common.primitives.Longs;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketTimeoutException;
@@ -49,9 +51,6 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.chromium.net.CronetEngine;
import org.chromium.net.CronetException;
import org.chromium.net.NetworkException;
@@ -295,13 +294,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
/* package */ final UrlRequest.Callback urlRequestCallback;
- private static final String TAG = "CronetDataSource";
- private static final String CONTENT_TYPE = "Content-Type";
- private static final String SET_COOKIE = "Set-Cookie";
- private static final String COOKIE = "Cookie";
-
- private static final Pattern CONTENT_RANGE_HEADER_PATTERN =
- Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
// The size of read buffer passed to cronet UrlRequest.read().
private static final int READ_BUFFER_SIZE_BYTES = 32 * 1024;
@@ -321,7 +313,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// Accessed by the calling thread only.
private boolean opened;
- private long bytesToSkip;
private long bytesRemaining;
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
@@ -556,8 +547,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
@Nullable IOException connectionOpenException = exception;
if (connectionOpenException != null) {
@Nullable String message = connectionOpenException.getMessage();
- if (message != null
- && Util.toLowerInvariant(message).contains("err_cleartext_not_permitted")) {
+ if (message != null && Ascii.toLowerCase(message).contains("err_cleartext_not_permitted")) {
throw new CleartextNotPermittedException(connectionOpenException, dataSpec);
}
throw new OpenException(connectionOpenException, dataSpec, getStatus(urlRequest));
@@ -573,11 +563,22 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// Check for a valid response code.
UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
int responseCode = responseInfo.getHttpStatusCode();
+ Map> responseHeaders = responseInfo.getAllHeaders();
if (responseCode < 200 || responseCode > 299) {
+ if (responseCode == 416) {
+ long documentSize =
+ HttpUtil.getDocumentSize(getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE));
+ if (dataSpec.position == documentSize) {
+ opened = true;
+ transferStarted(dataSpec);
+ return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0;
+ }
+ }
+
byte[] responseBody;
try {
responseBody = readResponseBody();
- } catch (HttpDataSourceException e) {
+ } catch (IOException e) {
responseBody = Util.EMPTY_BYTE_ARRAY;
}
@@ -585,7 +586,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
new InvalidResponseCodeException(
responseCode,
responseInfo.getHttpStatusText(),
- responseInfo.getAllHeaders(),
+ responseHeaders,
dataSpec,
responseBody);
if (responseCode == 416) {
@@ -597,8 +598,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// Check for a valid content type.
Predicate contentTypePredicate = this.contentTypePredicate;
if (contentTypePredicate != null) {
- List contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
- String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
+ @Nullable String contentType = getFirstHeader(responseHeaders, HttpHeaders.CONTENT_TYPE);
if (contentType != null && !contentTypePredicate.apply(contentType)) {
throw new InvalidContentTypeException(contentType, dataSpec);
}
@@ -607,14 +607,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// If we requested a range starting from a non-zero position and received a 200 rather than a
// 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
- bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
+ long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Calculate the content length.
if (!isCompressed(responseInfo)) {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;
} else {
- long contentLength = getContentLength(responseInfo);
+ long contentLength =
+ HttpUtil.getContentLength(
+ getFirstHeader(responseHeaders, HttpHeaders.CONTENT_LENGTH),
+ getFirstHeader(responseHeaders, HttpHeaders.CONTENT_RANGE));
bytesRemaining =
contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
}
@@ -627,6 +630,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
opened = true;
transferStarted(dataSpec);
+ try {
+ if (!skipFully(bytesToSkip)) {
+ throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
+ }
+ } catch (IOException e) {
+ throw new OpenException(e, dataSpec, Status.READING_RESPONSE);
+ }
+
return bytesRemaining;
}
@@ -641,34 +652,35 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
ByteBuffer readBuffer = getOrCreateReadBuffer();
- while (!readBuffer.hasRemaining()) {
+ if (!readBuffer.hasRemaining()) {
// Fill readBuffer with more data from Cronet.
operation.close();
readBuffer.clear();
- readInternal(readBuffer);
+ try {
+ readInternal(readBuffer);
+ } catch (IOException e) {
+ throw new HttpDataSourceException(
+ e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
+ }
if (finished) {
bytesRemaining = 0;
return C.RESULT_END_OF_INPUT;
- } else {
- // The operation didn't time out, fail or finish, and therefore data must have been read.
- readBuffer.flip();
- Assertions.checkState(readBuffer.hasRemaining());
- if (bytesToSkip > 0) {
- int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
- readBuffer.position(readBuffer.position() + bytesSkipped);
- bytesToSkip -= bytesSkipped;
- }
}
+
+ // The operation didn't time out, fail or finish, and therefore data must have been read.
+ readBuffer.flip();
+ Assertions.checkState(readBuffer.hasRemaining());
}
// 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);
+ (int)
+ Longs.min(
+ bytesRemaining != C.LENGTH_UNSET ? bytesRemaining : Long.MAX_VALUE,
+ readBuffer.remaining(),
+ readLength);
readBuffer.get(buffer, offset, bytesRead);
@@ -718,17 +730,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
int readLength = buffer.remaining();
if (readBuffer != null) {
- // Skip all the bytes we can from readBuffer if there are still bytes to skip.
- if (bytesToSkip != 0) {
- if (bytesToSkip >= readBuffer.remaining()) {
- bytesToSkip -= readBuffer.remaining();
- readBuffer.position(readBuffer.limit());
- } else {
- readBuffer.position(readBuffer.position() + (int) bytesToSkip);
- bytesToSkip = 0;
- }
- }
-
// If there is existing data in the readBuffer, read as much as possible. Return if any read.
int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer);
if (copyBytes != 0) {
@@ -740,44 +741,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
}
- boolean readMore = true;
- while (readMore) {
- // If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's
- // buffer. If we do not need to skip bytes, we may write to buffer directly.
- final boolean useCallerBuffer = bytesToSkip == 0;
-
- operation.close();
-
- if (!useCallerBuffer) {
- ByteBuffer readBuffer = getOrCreateReadBuffer();
- readBuffer.clear();
- if (bytesToSkip < READ_BUFFER_SIZE_BYTES) {
- readBuffer.limit((int) bytesToSkip);
- }
- }
-
- // Fill buffer with more data from Cronet.
- readInternal(useCallerBuffer ? buffer : castNonNull(readBuffer));
-
- if (finished) {
- bytesRemaining = 0;
- return C.RESULT_END_OF_INPUT;
- } else {
- // The operation didn't time out, fail or finish, and therefore data must have been read.
- Assertions.checkState(
- useCallerBuffer
- ? readLength > buffer.remaining()
- : castNonNull(readBuffer).position() > 0);
- // If we meant to skip bytes, subtract what was left and repeat, otherwise, continue.
- if (useCallerBuffer) {
- readMore = false;
- } else {
- bytesToSkip -= castNonNull(readBuffer).position();
- }
- }
+ // Fill buffer with more data from Cronet.
+ operation.close();
+ try {
+ readInternal(buffer);
+ } catch (IOException e) {
+ throw new HttpDataSourceException(
+ e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
}
- final int bytesRead = readLength - buffer.remaining();
+ if (finished) {
+ bytesRemaining = 0;
+ return C.RESULT_END_OF_INPUT;
+ }
+
+ // The operation didn't time out, fail or finish, and therefore data must have been read.
+ Assertions.checkState(readLength > buffer.remaining());
+ int bytesRead = readLength - buffer.remaining();
if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= bytesRead;
}
@@ -836,23 +816,16 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
requestBuilder.addHeader(key, value);
}
- if (dataSpec.httpBody != null && !requestHeaders.containsKey(CONTENT_TYPE)) {
+ if (dataSpec.httpBody != null && !requestHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) {
throw new IOException("HTTP request with non-empty body must set Content-Type");
}
- // Set the Range header.
- if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
- StringBuilder rangeValue = new StringBuilder();
- rangeValue.append("bytes=");
- rangeValue.append(dataSpec.position);
- rangeValue.append("-");
- if (dataSpec.length != C.LENGTH_UNSET) {
- rangeValue.append(dataSpec.position + dataSpec.length - 1);
- }
- requestBuilder.addHeader("Range", rangeValue.toString());
+ @Nullable String rangeHeader = buildRangeRequestHeader(dataSpec.position, dataSpec.length);
+ if (rangeHeader != null) {
+ requestBuilder.addHeader(HttpHeaders.RANGE, rangeHeader);
}
if (userAgent != null) {
- requestBuilder.addHeader("User-Agent", userAgent);
+ requestBuilder.addHeader(HttpHeaders.USER_AGENT, userAgent);
}
// TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed
// (adjusting the code as necessary).
@@ -885,13 +858,49 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
}
+ /**
+ * Attempts to skip the specified number of bytes in full.
+ *
+ * @param bytesToSkip The number of bytes to skip.
+ * @throws InterruptedIOException If the thread is interrupted during the operation.
+ * @throws IOException If an error occurs reading from the source.
+ * @return Whether the bytes were skipped in full. If {@code false} then the data ended before the
+ * specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}.
+ */
+ private boolean skipFully(long bytesToSkip) throws IOException {
+ if (bytesToSkip == 0) {
+ return true;
+ }
+ ByteBuffer readBuffer = getOrCreateReadBuffer();
+ while (bytesToSkip > 0) {
+ // Fill readBuffer with more data from Cronet.
+ operation.close();
+ readBuffer.clear();
+ readInternal(readBuffer);
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedIOException();
+ }
+ if (finished) {
+ return false;
+ } else {
+ // The operation didn't time out, fail or finish, and therefore data must have been read.
+ readBuffer.flip();
+ Assertions.checkState(readBuffer.hasRemaining());
+ int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
+ readBuffer.position(readBuffer.position() + bytesSkipped);
+ bytesToSkip -= bytesSkipped;
+ }
+ }
+ return true;
+ }
+
/**
* Reads the whole response body.
*
* @return The response body.
- * @throws HttpDataSourceException If an error occurs reading from the source.
+ * @throws IOException If an error occurs reading from the source.
*/
- private byte[] readResponseBody() throws HttpDataSourceException {
+ private byte[] readResponseBody() throws IOException {
byte[] responseBody = Util.EMPTY_BYTE_ARRAY;
ByteBuffer readBuffer = getOrCreateReadBuffer();
while (!finished) {
@@ -914,10 +923,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* the current {@code readBuffer} object so that it is not reused in the future.
*
* @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer.
- * @throws HttpDataSourceException If an error occurs reading from the source.
+ * @throws IOException If an error occurs reading from the source.
*/
@SuppressWarnings("ReferenceEquality")
- private void readInternal(ByteBuffer buffer) throws HttpDataSourceException {
+ private void readInternal(ByteBuffer buffer) throws IOException {
castNonNull(currentUrlRequest).read(buffer);
try {
if (!operation.block(readTimeoutMs)) {
@@ -930,23 +939,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
readBuffer = null;
}
Thread.currentThread().interrupt();
- throw new HttpDataSourceException(
- new InterruptedIOException(),
- castNonNull(currentDataSpec),
- HttpDataSourceException.TYPE_READ);
+ throw new InterruptedIOException();
} catch (SocketTimeoutException e) {
// The operation is ongoing so replace buffer to avoid it being written to by this
// operation during a subsequent request.
if (buffer == readBuffer) {
readBuffer = null;
}
- throw new HttpDataSourceException(
- e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
+ throw e;
}
if (exception != null) {
- throw new HttpDataSourceException(
- exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
+ throw exception;
}
}
@@ -967,52 +971,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return false;
}
- private static long getContentLength(UrlResponseInfo info) {
- long contentLength = C.LENGTH_UNSET;
- Map> headers = info.getAllHeaders();
- List contentLengthHeaders = headers.get("Content-Length");
- String contentLengthHeader = null;
- if (!isEmpty(contentLengthHeaders)) {
- contentLengthHeader = contentLengthHeaders.get(0);
- if (!TextUtils.isEmpty(contentLengthHeader)) {
- try {
- contentLength = Long.parseLong(contentLengthHeader);
- } catch (NumberFormatException e) {
- Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
- }
- }
- }
- List contentRangeHeaders = headers.get("Content-Range");
- if (!isEmpty(contentRangeHeaders)) {
- String contentRangeHeader = contentRangeHeaders.get(0);
- Matcher matcher = CONTENT_RANGE_HEADER_PATTERN.matcher(contentRangeHeader);
- if (matcher.find()) {
- try {
- long contentLengthFromRange =
- Long.parseLong(Assertions.checkNotNull(matcher.group(2)))
- - Long.parseLong(Assertions.checkNotNull(matcher.group(1)))
- + 1;
- if (contentLength < 0) {
- // Some proxy servers strip the Content-Length header. Fall back to the length
- // calculated here in this case.
- contentLength = contentLengthFromRange;
- } else if (contentLength != contentLengthFromRange) {
- // If there is a discrepancy between the Content-Length and Content-Range headers,
- // assume the one with the larger value is correct. We have seen cases where carrier
- // change one of them to reduce the size of a request, but it is unlikely anybody
- // would increase it.
- Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
- + "]");
- contentLength = max(contentLength, contentLengthFromRange);
- }
- } catch (NumberFormatException e) {
- Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
- }
- }
- }
- return contentLength;
- }
-
private static String parseCookies(List setCookieHeaders) {
return TextUtils.join(";", setCookieHeaders);
}
@@ -1021,7 +979,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (TextUtils.isEmpty(cookies)) {
return;
}
- requestBuilder.addHeader(COOKIE, cookies);
+ requestBuilder.addHeader(HttpHeaders.COOKIE, cookies);
}
private static int getStatus(UrlRequest request) throws InterruptedException {
@@ -1038,9 +996,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return statusHolder[0];
}
- @EnsuresNonNullIf(result = false, expression = "#1")
- private static boolean isEmpty(@Nullable List> list) {
- return list == null || list.isEmpty();
+ @Nullable
+ private static String getFirstHeader(Map> allHeaders, String headerName) {
+ @Nullable List headers = allHeaders.get(headerName);
+ return headers != null && !headers.isEmpty() ? headers.get(0) : null;
}
// Copy as much as possible from the src buffer into dst buffer.
@@ -1088,8 +1047,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return;
}
- List setCookieHeaders = info.getAllHeaders().get(SET_COOKIE);
- if (isEmpty(setCookieHeaders)) {
+ @Nullable List setCookieHeaders = info.getAllHeaders().get(HttpHeaders.SET_COOKIE);
+ if (setCookieHeaders == null || setCookieHeaders.isEmpty()) {
request.followRedirect();
return;
}
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 df3e9549e5..a446fcc299 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,7 +15,6 @@
*/
package com.google.android.exoplayer2.ext.cronet;
-
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
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 d9332342e3..de292006ec 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
@@ -33,9 +33,7 @@ import java.util.List;
import org.chromium.net.CronetEngine;
import org.chromium.net.CronetProvider;
-/**
- * A wrapper class for a {@link CronetEngine}.
- */
+/** A wrapper class for a {@link CronetEngine}. */
public final class CronetEngineWrapper {
private static final String TAG = "CronetEngineWrapper";
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 631e1300d6..5255163f8e 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
@@ -56,6 +56,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -66,6 +67,7 @@ import org.chromium.net.CronetEngine;
import org.chromium.net.NetworkException;
import org.chromium.net.UrlRequest;
import org.chromium.net.UrlResponseInfo;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -96,10 +98,10 @@ public final class CronetDataSourceTest {
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
@Mock private UrlRequest mockUrlRequest;
@Mock private TransferListener mockTransferListener;
- @Mock private Executor mockExecutor;
@Mock private NetworkException mockNetworkException;
@Mock private CronetEngine mockCronetEngine;
+ private ExecutorService executorService;
private CronetDataSource dataSourceUnderTest;
private boolean redirectCalled;
@@ -111,9 +113,10 @@ public final class CronetDataSourceTest {
defaultRequestProperties.put("defaultHeader1", "defaultValue1");
defaultRequestProperties.put("defaultHeader2", "defaultValue2");
+ executorService = Executors.newSingleThreadExecutor();
dataSourceUnderTest =
(CronetDataSource)
- new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor)
+ new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), executorService)
.setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
.setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
.setResetTimeoutOnRedirects(true)
@@ -143,6 +146,11 @@ public final class CronetDataSourceTest {
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
}
+ @After
+ public void tearDown() {
+ executorService.shutdown();
+ }
+
private UrlResponseInfo createUrlResponseInfo(int statusCode) {
return createUrlResponseInfoWithUrl(TEST_URL, statusCode);
}
@@ -256,6 +264,7 @@ public final class CronetDataSourceTest {
public void requestSetsRangeHeader() throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
mockResponseStartSuccess();
+ mockReadSuccess(0, 1000);
dataSourceUnderTest.open(testDataSpec);
// The header value to add is current position to current position + length - 1.
@@ -287,8 +296,6 @@ public final class CronetDataSourceTest {
testDataSpec =
new DataSpec.Builder()
.setUri(TEST_URL)
- .setPosition(1000)
- .setLength(5000)
.setHttpRequestHeaders(dataSpecRequestProperties)
.build();
mockResponseStartSuccess();
@@ -1160,7 +1167,7 @@ public final class CronetDataSourceTest {
throws HttpDataSourceException {
dataSourceUnderTest =
(CronetDataSource)
- new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor)
+ new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), executorService)
.setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
.setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
.setResetTimeoutOnRedirects(true)
@@ -1188,7 +1195,7 @@ public final class CronetDataSourceTest {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
dataSourceUnderTest =
(CronetDataSource)
- new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor)
+ new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), executorService)
.setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
.setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
.setResetTimeoutOnRedirects(true)
@@ -1198,6 +1205,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
mockSingleRedirectSuccess();
+ mockReadSuccess(0, 1000);
testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");
@@ -1224,7 +1232,7 @@ public final class CronetDataSourceTest {
throws HttpDataSourceException {
dataSourceUnderTest =
(CronetDataSource)
- new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), mockExecutor)
+ new CronetDataSource.Factory(new CronetEngineWrapper(mockCronetEngine), executorService)
.setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
.setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
.setResetTimeoutOnRedirects(true)
@@ -1368,7 +1376,7 @@ public final class CronetDataSourceTest {
@Test
public void allowDirectExecutor() throws HttpDataSourceException {
- testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
+ testDataSpec = new DataSpec(Uri.parse(TEST_URL));
mockResponseStartSuccess();
dataSourceUnderTest.open(testDataSpec);
@@ -1384,7 +1392,7 @@ public final class CronetDataSourceTest {
DefaultHttpDataSource.Factory fallbackFactory =
new DefaultHttpDataSource.Factory().setUserAgent("customFallbackFactoryUserAgent");
HttpDataSource dataSourceUnderTest =
- new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor())
+ new CronetDataSource.Factory(cronetEngineWrapper, executorService)
.setFallbackFactory(fallbackFactory)
.createDataSource();
@@ -1403,7 +1411,7 @@ public final class CronetDataSourceTest {
mockWebServer.enqueue(new MockResponse());
CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper((CronetEngine) null);
HttpDataSource dataSourceUnderTest =
- new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor())
+ new CronetDataSource.Factory(cronetEngineWrapper, executorService)
.setTransferListener(mockTransferListener)
.createDataSource();
DataSpec dataSpec =
@@ -1428,7 +1436,7 @@ public final class CronetDataSourceTest {
Map defaultRequestProperties = new HashMap<>();
defaultRequestProperties.put("0", "defaultRequestProperty0");
HttpDataSource dataSourceUnderTest =
- new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor())
+ new CronetDataSource.Factory(cronetEngineWrapper, executorService)
.setDefaultRequestProperties(defaultRequestProperties)
.createDataSource();
@@ -1450,7 +1458,7 @@ public final class CronetDataSourceTest {
Map defaultRequestProperties = new HashMap<>();
defaultRequestProperties.put("0", "defaultRequestProperty0");
CronetDataSource.Factory factory =
- new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor());
+ new CronetDataSource.Factory(cronetEngineWrapper, executorService);
HttpDataSource dataSourceUnderTest =
factory.setDefaultRequestProperties(defaultRequestProperties).createDataSource();
defaultRequestProperties.clear();
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java
index 0600254be5..eac96a9a31 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java
@@ -43,7 +43,7 @@ import java.util.List;
private final String codecName;
@Nullable private final byte[] extraData;
- private final @C.Encoding int encoding;
+ @C.Encoding private final int encoding;
private final int outputBufferSize;
private long nativeContext; // May be reassigned on resetting the codec.
@@ -98,7 +98,8 @@ import java.util.List;
}
@Override
- protected @Nullable FfmpegDecoderException decode(
+ @Nullable
+ protected FfmpegDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
nativeContext = ffmpegReset(nativeContext, extraData);
@@ -159,7 +160,8 @@ import java.util.List;
}
/** Returns the encoding of output audio. */
- public @C.Encoding int getEncoding() {
+ @C.Encoding
+ public int getEncoding() {
return encoding;
}
@@ -167,7 +169,8 @@ import java.util.List;
* Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if
* not required.
*/
- private static @Nullable byte[] getExtraData(String mimeType, List initializationData) {
+ @Nullable
+ private static byte[] getExtraData(String mimeType, List initializationData) {
switch (mimeType) {
case MimeTypes.AUDIO_AAC:
case MimeTypes.AUDIO_OPUS:
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 6fb47e962b..798c936e2e 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
@@ -23,9 +23,7 @@ import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
-/**
- * Configures and queries the underlying native library.
- */
+/** Configures and queries the underlying native library. */
public final class FfmpegLibrary {
static {
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
index bbcc26fb64..a52f8088b3 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
@@ -24,9 +24,11 @@ import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Renderer;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.DefaultAudioSink;
@@ -87,13 +89,13 @@ public class FlacPlaybackTest {
"audiosinkdumps/" + fileName + ".audiosink.dump");
}
- private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
+ private static class TestPlaybackRunnable implements Player.Listener, Runnable {
private final Context context;
private final Uri uri;
private final AudioSink audioSink;
- @Nullable private ExoPlayer player;
+ @Nullable private SimpleExoPlayer player;
@Nullable private ExoPlaybackException playbackException;
public TestPlaybackRunnable(Uri uri, Context context, AudioSink audioSink) {
@@ -105,9 +107,16 @@ public class FlacPlaybackTest {
@Override
public void run() {
Looper.prepare();
- LibflacAudioRenderer audioRenderer =
- new LibflacAudioRenderer(/* eventHandler= */ null, /* eventListener= */ null, audioSink);
- player = new ExoPlayer.Builder(context, audioRenderer).build();
+ RenderersFactory renderersFactory =
+ (eventHandler,
+ videoRendererEventListener,
+ audioRendererEventListener,
+ textRendererOutput,
+ metadataRendererOutput) ->
+ new Renderer[] {
+ new LibflacAudioRenderer(eventHandler, audioRendererEventListener, audioSink)
+ };
+ player = new SimpleExoPlayer.Builder(context, renderersFactory).build();
player.addListener(this);
MediaSource mediaSource =
new ProgressiveMediaSource.Factory(
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
index 0ac4dbeffa..99e74d64bb 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
@@ -45,9 +45,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
-/**
- * Facilitates the extraction of data from the FLAC container format.
- */
+/** Facilitates the extraction of data from the FLAC container format. */
public final class FlacExtractor implements Extractor {
/** Factory that returns one extractor which is a {@link FlacExtractor}. */
@@ -75,7 +73,7 @@ public final class FlacExtractor implements Extractor {
*/
public static final int FLAG_DISABLE_ID3_METADATA =
com.google.android.exoplayer2.extractor.flac.FlacExtractor.FLAG_DISABLE_ID3_METADATA;
- // LINT.ThenChange(../../../../../../../../../../../library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java)
+ // LINT.ThenChange(../../../../../../../../../../extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java)
private final ParsableByteArray outputBuffer;
private final boolean id3MetadataDisabled;
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java
index d8b9b808a6..8a2b14d366 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java
@@ -18,9 +18,7 @@ package com.google.android.exoplayer2.ext.flac;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
-/**
- * Configures and queries the underlying native library.
- */
+/** Configures and queries the underlying native library. */
public final class FlacLibrary {
static {
diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc
index 850f6883bf..50db06f436 100644
--- a/extensions/flac/src/main/jni/flac_jni.cc
+++ b/extensions/flac/src/main/jni/flac_jni.cc
@@ -47,6 +47,7 @@ class JavaDataSource : public DataSource {
if (mid == NULL) {
jclass cls = env->GetObjectClass(flacDecoderJni);
mid = env->GetMethodID(cls, "read", "(Ljava/nio/ByteBuffer;)I");
+ env->DeleteLocalRef(cls);
}
}
@@ -57,6 +58,7 @@ class JavaDataSource : public DataSource {
// Exception is thrown in Java when returning from the native call.
result = -1;
}
+ env->DeleteLocalRef(byteBuffer);
return result;
}
diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle
index 891888a0d2..db508e5137 100644
--- a/extensions/gvr/build.gradle
+++ b/extensions/gvr/build.gradle
@@ -17,7 +17,6 @@ android.defaultConfig.minSdkVersion 19
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation project(modulePrefix + 'library-ui')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
api 'com.google.vr:sdk-base:1.190.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
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 839c832951..6ddc317274 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
@@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.net.Uri;
import android.view.Surface;
-import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -29,7 +28,6 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
-import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline.Window;
@@ -38,8 +36,6 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.ads.AdsLoader;
-import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.testutil.ActionSchedule;
import com.google.android.exoplayer2.testutil.ExoHostedTest;
@@ -51,7 +47,6 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
-import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -190,7 +185,7 @@ public final class ImaPlaybackTest {
}
}
- private static final class ImaHostedTest extends ExoHostedTest implements EventListener {
+ private static final class ImaHostedTest extends ExoHostedTest implements Player.Listener {
private final Uri contentUri;
private final DataSpec adTagDataSpec;
@@ -251,18 +246,7 @@ public final class ImaPlaybackTest {
/* adsId= */ adTagDataSpec.uri,
new DefaultMediaSourceFactory(dataSourceFactory),
Assertions.checkNotNull(imaAdsLoader),
- new AdViewProvider() {
-
- @Override
- public ViewGroup getAdViewGroup() {
- return overlayFrameLayout;
- }
-
- @Override
- public ImmutableList getAdOverlayInfos() {
- return ImmutableList.of();
- }
- });
+ () -> overlayFrameLayout);
}
@Override
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
index 7275da7230..6690fa95ec 100644
--- 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
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.ima;
+import static com.google.android.exoplayer2.Player.COMMAND_GET_VOLUME;
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;
@@ -55,11 +56,12 @@ 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.trackselection.TrackSelectionUtil;
+import com.google.android.exoplayer2.ui.AdOverlayInfo;
+import com.google.android.exoplayer2.ui.AdViewProvider;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
@@ -74,7 +76,7 @@ import java.util.List;
import java.util.Map;
/** Handles loading and playback of a single ad tag. */
-/* package */ final class AdTagLoader implements Player.EventListener {
+/* package */ final class AdTagLoader implements Player.Listener {
private static final String TAG = "AdTagLoader";
@@ -317,7 +319,7 @@ import java.util.Map;
new AdPlaybackState(adsId, getAdGroupTimesUsForCuePoints(adsManager.getAdCuePoints()));
updateAdPlaybackState();
}
- for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) {
+ for (AdOverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) {
adDisplayContainer.registerFriendlyObstruction(
imaFactory.createFriendlyObstruction(
overlayInfo.view,
@@ -467,7 +469,10 @@ import java.util.Map;
}
@Override
- public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ public void onPositionDiscontinuity(
+ Player.PositionInfo oldPosition,
+ Player.PositionInfo newPosition,
+ @Player.DiscontinuityReason int reason) {
handleTimelineOrPositionChanged();
}
@@ -696,19 +701,13 @@ import java.util.Map;
return lastVolumePercent;
}
- @Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
- if (audioComponent != null) {
- return (int) (audioComponent.getVolume() * 100);
+ if (player.isCommandAvailable(COMMAND_GET_VOLUME)) {
+ return (int) (player.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;
+ return TrackSelectionUtil.hasTrackOfType(trackSelections, C.TRACK_TYPE_AUDIO) ? 100 : 0;
}
private void handleAdEvent(AdEvent adEvent) {
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 336a560042..72cae676af 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
@@ -48,6 +48,7 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
+import com.google.android.exoplayer2.ui.AdViewProvider;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@@ -84,7 +85,7 @@ import java.util.Set;
* 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, AdsLoader {
+public final class ImaAdsLoader implements Player.Listener, AdsLoader {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
@@ -600,7 +601,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception);
}
- // Player.EventListener implementation.
+ // Player.Listener implementation.
@Override
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
@@ -613,7 +614,10 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
}
@Override
- public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ public void onPositionDiscontinuity(
+ Player.PositionInfo oldPosition,
+ Player.PositionInfo newPosition,
+ @Player.DiscontinuityReason int reason) {
maybeUpdateCurrentAdTagLoader();
maybePreloadNextPeriodAds();
}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java
index 0324e93713..377a9c4db8 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java
@@ -36,7 +36,7 @@ import com.google.ads.interactivemedia.v3.api.UiElement;
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.source.ads.AdsLoader.OverlayInfo;
+import com.google.android.exoplayer2.ui.AdOverlayInfo;
import com.google.android.exoplayer2.upstream.DataSchemeDataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Util;
@@ -138,18 +138,18 @@ import java.util.Set;
/**
* Returns the IMA {@link FriendlyObstructionPurpose} corresponding to the given {@link
- * OverlayInfo#purpose}.
+ * AdOverlayInfo#purpose}.
*/
public static FriendlyObstructionPurpose getFriendlyObstructionPurpose(
- @OverlayInfo.Purpose int purpose) {
+ @AdOverlayInfo.Purpose int purpose) {
switch (purpose) {
- case OverlayInfo.PURPOSE_CONTROLS:
+ case AdOverlayInfo.PURPOSE_CONTROLS:
return FriendlyObstructionPurpose.VIDEO_CONTROLS;
- case OverlayInfo.PURPOSE_CLOSE_AD:
+ case AdOverlayInfo.PURPOSE_CLOSE_AD:
return FriendlyObstructionPurpose.CLOSE_AD;
- case OverlayInfo.PURPOSE_NOT_VISIBLE:
+ case AdOverlayInfo.PURPOSE_NOT_VISIBLE:
return FriendlyObstructionPurpose.NOT_VISIBLE;
- case OverlayInfo.PURPOSE_OTHER:
+ case AdOverlayInfo.PURPOSE_OTHER:
default:
return FriendlyObstructionPurpose.OTHER;
}
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
index 6b62af93f3..0582423a91 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
@@ -27,8 +27,10 @@ import com.google.android.exoplayer2.util.ListenerSet;
/** A fake player for testing content/ad playback. */
/* package */ final class FakePlayer extends StubExoPlayer {
- private final ListenerSet listeners;
+ private final ListenerSet listeners;
private final Timeline.Period period;
+ private final Object windowUid = new Object();
+ private final Object periodUid = new Object();
private Timeline timeline;
@Player.State private int state;
@@ -45,8 +47,7 @@ import com.google.android.exoplayer2.util.ListenerSet;
new ListenerSet<>(
Looper.getMainLooper(),
Clock.DEFAULT,
- Player.Events::new,
- (listener, eventFlags) -> listener.onEvents(/* player= */ this, eventFlags));
+ (listener, flags) -> listener.onEvents(/* player= */ this, new Events(flags)));
period = new Timeline.Period();
state = Player.STATE_IDLE;
playWhenReady = true;
@@ -66,6 +67,16 @@ import com.google.android.exoplayer2.util.ListenerSet;
*/
public void setPlayingContentPosition(int periodIndex, long positionMs) {
boolean notify = isPlayingAd;
+ PositionInfo oldPosition =
+ new PositionInfo(
+ windowUid,
+ /* windowIndex= */ 0,
+ periodUid,
+ /* periodIndex= */ 0,
+ this.positionMs,
+ this.contentPositionMs,
+ this.adGroupIndex,
+ this.adIndexInAdGroup);
isPlayingAd = false;
adGroupIndex = C.INDEX_UNSET;
adIndexInAdGroup = C.INDEX_UNSET;
@@ -73,9 +84,21 @@ import com.google.android.exoplayer2.util.ListenerSet;
this.positionMs = positionMs;
contentPositionMs = positionMs;
if (notify) {
+ PositionInfo newPosition =
+ new PositionInfo(
+ windowUid,
+ /* windowIndex= */ 0,
+ periodUid,
+ /* periodIndex= */ 0,
+ positionMs,
+ this.contentPositionMs,
+ this.adGroupIndex,
+ this.adIndexInAdGroup);
listeners.sendEvent(
Player.EVENT_POSITION_DISCONTINUITY,
- listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION));
+ listener ->
+ listener.onPositionDiscontinuity(
+ oldPosition, newPosition, DISCONTINUITY_REASON_AUTO_TRANSITION));
}
}
@@ -91,6 +114,16 @@ import com.google.android.exoplayer2.util.ListenerSet;
long positionMs,
long contentPositionMs) {
boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup;
+ PositionInfo oldPosition =
+ new PositionInfo(
+ windowUid,
+ /* windowIndex= */ 0,
+ periodUid,
+ /* periodIndex= */ 0,
+ this.positionMs,
+ this.contentPositionMs,
+ this.adGroupIndex,
+ this.adIndexInAdGroup);
isPlayingAd = true;
this.periodIndex = periodIndex;
this.adGroupIndex = adGroupIndex;
@@ -98,9 +131,21 @@ import com.google.android.exoplayer2.util.ListenerSet;
this.positionMs = positionMs;
this.contentPositionMs = contentPositionMs;
if (notify) {
+ PositionInfo newPosition =
+ new PositionInfo(
+ windowUid,
+ /* windowIndex= */ 0,
+ periodUid,
+ /* periodIndex= */ 0,
+ positionMs,
+ contentPositionMs,
+ adGroupIndex,
+ adIndexInAdGroup);
listeners.sendEvent(
EVENT_POSITION_DISCONTINUITY,
- listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION));
+ listener ->
+ listener.onPositionDiscontinuity(
+ oldPosition, newPosition, DISCONTINUITY_REASON_AUTO_TRANSITION));
}
}
@@ -140,15 +185,20 @@ import com.google.android.exoplayer2.util.ListenerSet;
}
@Override
- public void addListener(Player.EventListener listener) {
+ public void addListener(Player.Listener listener) {
listeners.add(listener);
}
@Override
- public void removeListener(Player.EventListener listener) {
+ public void removeListener(Player.Listener listener) {
listeners.remove(listener);
}
+ @Override
+ public Commands getAvailableCommands() {
+ return Commands.EMPTY;
+ }
+
@Override
@Player.State
public int getPlaybackState() {
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
index e7b6603694..6b62b38e4d 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
@@ -68,6 +68,8 @@ import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
+import com.google.android.exoplayer2.ui.AdOverlayInfo;
+import com.google.android.exoplayer2.ui.AdViewProvider;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@@ -129,8 +131,8 @@ public final class ImaAdsLoaderTest {
private TimelineWindowDefinition[] timelineWindowDefinitions;
private AdsMediaSource adsMediaSource;
private ViewGroup adViewGroup;
- private AdsLoader.AdViewProvider adViewProvider;
- private AdsLoader.AdViewProvider audioAdsAdViewProvider;
+ private AdViewProvider adViewProvider;
+ private AdViewProvider audioAdsAdViewProvider;
private AdEvent.AdEventListener adEventListener;
private ContentProgressProvider contentProgressProvider;
private VideoAdPlayer videoAdPlayer;
@@ -145,30 +147,19 @@ public final class ImaAdsLoaderTest {
adViewGroup = new FrameLayout(getApplicationContext());
View adOverlayView = new View(getApplicationContext());
adViewProvider =
- new AdsLoader.AdViewProvider() {
+ new AdViewProvider() {
@Override
public ViewGroup getAdViewGroup() {
return adViewGroup;
}
@Override
- public ImmutableList getAdOverlayInfos() {
+ public ImmutableList getAdOverlayInfos() {
return ImmutableList.of(
- new AdsLoader.OverlayInfo(adOverlayView, AdsLoader.OverlayInfo.PURPOSE_CLOSE_AD));
- }
- };
- audioAdsAdViewProvider =
- new AdsLoader.AdViewProvider() {
- @Override
- public ViewGroup getAdViewGroup() {
- return null;
- }
-
- @Override
- public ImmutableList getAdOverlayInfos() {
- return ImmutableList.of();
+ new AdOverlayInfo(adOverlayView, AdOverlayInfo.PURPOSE_CLOSE_AD));
}
};
+ audioAdsAdViewProvider = () -> null;
imaAdsLoader =
new ImaAdsLoader.Builder(getApplicationContext())
.setImaFactory(mockImaFactory)
@@ -281,7 +272,26 @@ public final class ImaAdsLoaderTest {
videoAdPlayer.pauseAd(TEST_AD_MEDIA_INFO);
videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException()));
- imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
+ imaAdsLoader.onPositionDiscontinuity(
+ new Player.PositionInfo(
+ /* windowUid= */ new Object(),
+ /* windowIndex= */ 0,
+ /* periodUid= */ new Object(),
+ /* periodIndex= */ 0,
+ /* positionMs= */ 10_000,
+ /* contentPositionMs= */ 0,
+ /* adGroupIndex= */ -1,
+ /* adIndexInAdGroup= */ -1),
+ new Player.PositionInfo(
+ /* windowUid= */ new Object(),
+ /* windowIndex= */ 1,
+ /* periodUid= */ new Object(),
+ /* periodIndex= */ 0,
+ /* positionMs= */ 20_000,
+ /* contentPositionMs= */ 0,
+ /* adGroupIndex= */ -1,
+ /* adIndexInAdGroup= */ -1),
+ Player.DISCONTINUITY_REASON_SEEK);
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
imaAdsLoader.handlePrepareError(
adsMediaSource, /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException());
diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md
deleted file mode 100644
index 9e26c07c5d..0000000000
--- a/extensions/jobdispatcher/README.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# ExoPlayer Firebase JobDispatcher extension #
-
-**This extension is deprecated. Use the [WorkManager extension][] instead.**
-
-This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
-
-[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
-[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
-
-## Getting the extension ##
-
-The easiest way to use the extension is to add it as a gradle dependency:
-
-```gradle
-implementation 'com.google.android.exoplayer:extension-jobdispatcher:2.X.X'
-```
-
-where `2.X.X` is the version, which must match the version of the ExoPlayer
-library being used.
-
-Alternatively, you can clone the ExoPlayer repository and depend on the module
-locally. Instructions for doing this can be found in ExoPlayer's
-[top level README][].
-
-[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
diff --git a/extensions/jobdispatcher/build.gradle b/extensions/jobdispatcher/build.gradle
deleted file mode 100644
index df50cde8f9..0000000000
--- a/extensions/jobdispatcher/build.gradle
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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.
- */
-apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
-
-dependencies {
- implementation project(modulePrefix + 'library-core')
- implementation 'com.firebase:firebase-jobdispatcher:0.8.5'
- compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
-}
-
-ext {
- javadocTitle = 'Firebase JobDispatcher extension'
-}
-apply from: '../../javadoc_library.gradle'
-
-ext {
- releaseArtifact = 'extension-jobdispatcher'
- releaseDescription = 'Firebase JobDispatcher extension for ExoPlayer.'
-}
-apply from: '../../publish.gradle'
diff --git a/extensions/jobdispatcher/src/main/AndroidManifest.xml b/extensions/jobdispatcher/src/main/AndroidManifest.xml
deleted file mode 100644
index 306a087e6c..0000000000
--- a/extensions/jobdispatcher/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
deleted file mode 100644
index b65988a5e2..0000000000
--- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * 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.jobdispatcher;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import com.firebase.jobdispatcher.Constraint;
-import com.firebase.jobdispatcher.FirebaseJobDispatcher;
-import com.firebase.jobdispatcher.GooglePlayDriver;
-import com.firebase.jobdispatcher.Job;
-import com.firebase.jobdispatcher.JobParameters;
-import com.firebase.jobdispatcher.JobService;
-import com.firebase.jobdispatcher.Lifetime;
-import com.google.android.exoplayer2.scheduler.Requirements;
-import com.google.android.exoplayer2.scheduler.Scheduler;
-import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.Log;
-import com.google.android.exoplayer2.util.Util;
-
-/**
- * A {@link Scheduler} that uses {@link FirebaseJobDispatcher}. To use this scheduler, you must add
- * {@link JobDispatcherSchedulerService} to your manifest:
- *
- *
{@literal
- *
- *
- *
- *
- *
- *
- *
- *
- * }
- *
- *
This Scheduler uses Google Play services but does not do any availability checks. Any uses
- * should be guarded with a call to {@code
- * GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)}
- *
- * @see GoogleApiAvailability
- * @deprecated Use com.google.android.exoplayer2.ext.workmanager.WorkManagerScheduler or {@link
- * com.google.android.exoplayer2.scheduler.PlatformScheduler}.
- */
-@Deprecated
-public final class JobDispatcherScheduler implements Scheduler {
-
- private static final String TAG = "JobDispatcherScheduler";
- private static final String KEY_SERVICE_ACTION = "service_action";
- private static final String KEY_SERVICE_PACKAGE = "service_package";
- private static final String KEY_REQUIREMENTS = "requirements";
- private static final int SUPPORTED_REQUIREMENTS =
- Requirements.NETWORK
- | Requirements.NETWORK_UNMETERED
- | Requirements.DEVICE_IDLE
- | Requirements.DEVICE_CHARGING;
-
- private final String jobTag;
- private final FirebaseJobDispatcher jobDispatcher;
-
- /**
- * @param context A context.
- * @param jobTag A tag for jobs scheduled by this instance. If the same tag was used by a previous
- * instance, anything scheduled by the previous instance will be canceled by this instance if
- * {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called.
- */
- public JobDispatcherScheduler(Context context, String jobTag) {
- context = context.getApplicationContext();
- this.jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context));
- this.jobTag = jobTag;
- }
-
- @Override
- public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
- Job job = buildJob(jobDispatcher, requirements, jobTag, servicePackage, serviceAction);
- int result = jobDispatcher.schedule(job);
- return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
- }
-
- @Override
- public boolean cancel() {
- int result = jobDispatcher.cancel(jobTag);
- return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
- }
-
- @Override
- public Requirements getSupportedRequirements(Requirements requirements) {
- return requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
- }
-
- private static Job buildJob(
- FirebaseJobDispatcher dispatcher,
- Requirements requirements,
- String tag,
- String servicePackage,
- String serviceAction) {
- Requirements filteredRequirements = requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
- if (!filteredRequirements.equals(requirements)) {
- Log.w(
- TAG,
- "Ignoring unsupported requirements: "
- + (filteredRequirements.getRequirements() ^ requirements.getRequirements()));
- }
-
- Job.Builder builder =
- dispatcher
- .newJobBuilder()
- .setService(JobDispatcherSchedulerService.class) // the JobService that will be called
- .setTag(tag);
- if (requirements.isUnmeteredNetworkRequired()) {
- builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
- } else if (requirements.isNetworkRequired()) {
- builder.addConstraint(Constraint.ON_ANY_NETWORK);
- }
- if (requirements.isIdleRequired()) {
- builder.addConstraint(Constraint.DEVICE_IDLE);
- }
- if (requirements.isChargingRequired()) {
- builder.addConstraint(Constraint.DEVICE_CHARGING);
- }
- builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true);
-
- Bundle extras = new Bundle();
- extras.putString(KEY_SERVICE_ACTION, serviceAction);
- extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
- extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
- builder.setExtras(extras);
-
- return builder.build();
- }
-
- /** A {@link JobService} that starts the target service if the requirements are met. */
- public static final class JobDispatcherSchedulerService extends JobService {
- @Override
- public boolean onStartJob(JobParameters params) {
- Bundle extras = Assertions.checkNotNull(params.getExtras());
- Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
- int notMetRequirements = requirements.getNotMetRequirements(this);
- if (notMetRequirements == 0) {
- String serviceAction = Assertions.checkNotNull(extras.getString(KEY_SERVICE_ACTION));
- String servicePackage = Assertions.checkNotNull(extras.getString(KEY_SERVICE_PACKAGE));
- Intent intent = new Intent(serviceAction).setPackage(servicePackage);
- Util.startForegroundService(this, intent);
- } else {
- Log.w(TAG, "Requirements not met: " + notMetRequirements);
- jobFinished(params, /* needsReschedule */ true);
- }
- return false;
- }
-
- @Override
- public boolean onStopJob(JobParameters params) {
- return false;
- }
- }
-}
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java
deleted file mode 100644
index a66904b505..0000000000
--- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2019 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.
- */
-@NonNullApi
-package com.google.android.exoplayer2.ext.jobdispatcher;
-
-import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle
index 14ced09f12..a09e6d2f7c 100644
--- a/extensions/leanback/build.gradle
+++ b/extensions/leanback/build.gradle
@@ -16,7 +16,7 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
android.defaultConfig.minSdkVersion 17
dependencies {
- implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-common')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.leanback:leanback:1.0.0'
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
index 6da02bb324..f2fc3279b8 100644
--- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
+++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
@@ -37,7 +37,6 @@ import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.Util;
-import com.google.android.exoplayer2.video.VideoListener;
/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */
public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnable {
@@ -49,7 +48,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
private final Context context;
private final Player player;
private final Handler handler;
- private final ComponentListener componentListener;
+ private final PlayerListener playerListener;
private final int updatePeriodMs;
@Nullable private PlaybackPreparer playbackPreparer;
@@ -73,7 +72,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
this.player = player;
this.updatePeriodMs = updatePeriodMs;
handler = Util.createHandlerForCurrentOrMainLooper();
- componentListener = new ComponentListener();
+ playerListener = new PlayerListener();
controlDispatcher = new DefaultControlDispatcher();
}
@@ -118,23 +117,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
public void onAttachedToHost(PlaybackGlueHost host) {
if (host instanceof SurfaceHolderGlueHost) {
surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host);
- surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener);
+ surfaceHolderGlueHost.setSurfaceHolderCallback(playerListener);
}
notifyStateChanged();
- player.addListener(componentListener);
- Player.VideoComponent videoComponent = player.getVideoComponent();
- if (videoComponent != null) {
- videoComponent.addVideoListener(componentListener);
- }
+ player.addListener(playerListener);
}
@Override
public void onDetachedFromHost() {
- player.removeListener(componentListener);
- Player.VideoComponent videoComponent = player.getVideoComponent();
- if (videoComponent != null) {
- videoComponent.removeVideoListener(componentListener);
- }
+ player.removeListener(playerListener);
if (surfaceHolderGlueHost != null) {
removeSurfaceHolderCallback(surfaceHolderGlueHost);
surfaceHolderGlueHost = null;
@@ -227,10 +218,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
/* package */ void setVideoSurface(@Nullable Surface surface) {
hasSurface = surface != null;
- Player.VideoComponent videoComponent = player.getVideoComponent();
- if (videoComponent != null) {
- videoComponent.setVideoSurface(surface);
- }
+ player.setVideoSurface(surface);
maybeNotifyPreparedStateChanged(getCallback());
}
@@ -258,8 +246,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
}
- private final class ComponentListener
- implements Player.EventListener, SurfaceHolder.Callback, VideoListener {
+ private final class PlayerListener implements Player.Listener, SurfaceHolder.Callback {
// SurfaceHolder.Callback implementation.
@@ -306,7 +293,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
}
@Override
- public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
+ public void onPositionDiscontinuity(
+ Player.PositionInfo oldPosition,
+ Player.PositionInfo newPosition,
+ @DiscontinuityReason int reason) {
Callback callback = getCallback();
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
diff --git a/extensions/media2/build.gradle b/extensions/media2/build.gradle
index a89354d7b3..da70210bd6 100644
--- a/extensions/media2/build.gradle
+++ b/extensions/media2/build.gradle
@@ -13,16 +13,15 @@
// limitations under the License.
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
-android.defaultConfig.minSdkVersion 19
-
dependencies {
- implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-common')
implementation 'androidx.collection:collection:' + androidxCollectionVersion
implementation 'androidx.concurrent:concurrent-futures:' + androidxFuturesVersion
api 'androidx.media2:media2-session:' + androidxMedia2Version
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
+ androidTestImplementation project(modulePrefix + 'library-core')
androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
androidTestImplementation 'androidx.test:core:' + androidxTestCoreVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java
index edabd55812..d4e996caa2 100644
--- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java
+++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java
@@ -28,11 +28,8 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.media.AudioManager;
-import android.os.Build;
-import android.os.Build.VERSION_CODES;
import android.os.Looper;
import androidx.annotation.Nullable;
-import androidx.core.util.ObjectsCompat;
import androidx.media.AudioAttributesCompat;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
@@ -43,7 +40,6 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
-import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.android.exoplayer2.ControlDispatcher;
@@ -52,6 +48,7 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.media2.test.R;
import com.google.android.exoplayer2.upstream.RawResourceDataSource;
+import com.google.android.exoplayer2.util.Util;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Arrays;
@@ -93,7 +90,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_onceWithAudioResource_changesPlayerStateToPlaying() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
@@ -120,7 +116,6 @@ public class SessionPlayerConnectorTest {
@Test
@MediumTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_onceWithAudioResourceOnMainThread_notifiesOnPlayerStateChanged()
throws Exception {
CountDownLatch onPlayerStatePlayingLatch = new CountDownLatch(1);
@@ -158,7 +153,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_withCustomControlDispatcher_isSkipped() throws Exception {
if (Looper.myLooper() == null) {
Looper.prepare();
@@ -194,7 +188,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setMediaItem_withAudioResource_notifiesOnPlaybackCompleted() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
@@ -219,7 +212,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setMediaItem_withVideoResource_notifiesOnPlaybackCompleted() throws Exception {
TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector);
CountDownLatch onPlaybackCompletedLatch = new CountDownLatch(1);
@@ -243,7 +235,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getDuration_whenIdleState_returnsUnknownTime() {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
assertThat(sessionPlayerConnector.getDuration()).isEqualTo(SessionPlayer.UNKNOWN_TIME);
@@ -251,7 +242,6 @@ public class SessionPlayerConnectorTest {
@Test
@MediumTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getDuration_afterPrepared_returnsDuration() throws Exception {
TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector);
@@ -263,7 +253,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getCurrentPosition_whenIdleState_returnsDefaultPosition() {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
assertThat(sessionPlayerConnector.getCurrentPosition()).isEqualTo(0);
@@ -271,7 +260,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getBufferedPosition_whenIdleState_returnsDefaultPosition() {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
assertThat(sessionPlayerConnector.getBufferedPosition()).isEqualTo(0);
@@ -279,7 +267,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getPlaybackSpeed_whenIdleState_throwsNoException() {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(SessionPlayer.PLAYER_STATE_IDLE);
try {
@@ -291,7 +278,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_withDataSourceCallback_changesPlayerState() throws Exception {
sessionPlayerConnector.setMediaItem(TestUtils.createMediaItem(R.raw.video_big_buck_bunny));
sessionPlayerConnector.prepare();
@@ -308,7 +294,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setMediaItem_withNullMediaItem_throwsException() {
try {
sessionPlayerConnector.setMediaItem(null);
@@ -320,7 +305,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaybackSpeed_afterPlayback_remainsSame() throws Exception {
int resId1 = R.raw.video_big_buck_bunny;
MediaItem mediaItem1 =
@@ -363,7 +347,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void seekTo_withSeriesOfSeek_succeeds() throws Exception {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
@@ -378,7 +361,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void seekTo_skipsUnnecessarySeek() throws Exception {
CountDownLatch readAllowedLatch = new CountDownLatch(1);
playerTestRule.setDataSourceInstrumentation(
@@ -435,7 +417,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void seekTo_whenUnderlyingPlayerAlsoSeeks_throwsNoException() throws Exception {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
@@ -456,7 +437,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void seekTo_byUnderlyingPlayer_notifiesOnSeekCompleted() throws Exception {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
@@ -484,7 +464,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void getPlayerState_withCallingPrepareAndPlayAndPause_reflectsPlayerState()
throws Throwable {
TestUtils.loadResource(R.raw.video_desks, sessionPlayerConnector);
@@ -521,7 +500,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = VERSION_CODES.KITKAT)
public void prepare_twice_finishes() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
@@ -530,7 +508,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void prepare_notifiesOnPlayerStateChanged() throws Throwable {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
@@ -552,7 +529,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void prepare_notifiesBufferingCompletedOnce() throws Throwable {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
@@ -587,7 +563,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void seekTo_whenPrepared_notifiesOnSeekCompleted() throws Throwable {
long mp4DurationMs = 8_484L;
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
@@ -611,7 +586,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaybackSpeed_whenPrepared_notifiesOnPlaybackSpeedChanged() throws Throwable {
TestUtils.loadResource(R.raw.video_big_buck_bunny, sessionPlayerConnector);
@@ -636,7 +610,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaybackSpeed_withZeroSpeed_throwsException() {
try {
sessionPlayerConnector.setPlaybackSpeed(0.0f);
@@ -648,7 +621,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaybackSpeed_withNegativeSpeed_throwsException() {
try {
sessionPlayerConnector.setPlaybackSpeed(-1.0f);
@@ -660,7 +632,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void close_throwsNoExceptionAndDoesNotCrash() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
AudioAttributesCompat attributes =
@@ -679,7 +650,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void cancelReturnedFuture_withSeekTo_cancelsPendingCommand() throws Exception {
CountDownLatch readRequestedLatch = new CountDownLatch(1);
CountDownLatch readAllowedLatch = new CountDownLatch(1);
@@ -719,7 +689,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_withNullPlaylist_throwsException() throws Exception {
try {
sessionPlayerConnector.setPlaylist(null, null);
@@ -731,7 +700,6 @@ public class SessionPlayerConnectorTest {
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_withPlaylistContainingNullItem_throwsException() {
try {
List list = new ArrayList<>();
@@ -745,7 +713,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_setsPlaylistAndCurrentMediaItem() throws Exception {
List playlist = TestUtils.createPlaylist(10);
PlayerCallbackForPlaylist callback = new PlayerCallbackForPlaylist(playlist, 1);
@@ -760,7 +727,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylistAndRemoveAllPlaylistItem_playerStateBecomesIdle() throws Exception {
List playlist = new ArrayList<>();
playlist.add(TestUtils.createMediaItem(R.raw.video_1));
@@ -786,7 +752,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List playlist = TestUtils.createPlaylist(10);
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(2);
@@ -811,7 +776,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_byUnderlyingPlayerBeforePrepare_notifiesOnPlaylistChanged()
throws Exception {
List playlistToExoPlayer = TestUtils.createPlaylist(4);
@@ -830,7 +794,7 @@ public class SessionPlayerConnectorTest {
SessionPlayer player,
@Nullable List list,
@Nullable MediaMetadata metadata) {
- if (ObjectsCompat.equals(list, playlistToExoPlayer)) {
+ if (Util.areEqual(list, playlistToExoPlayer)) {
onPlaylistChangedLatch.countDown();
}
}
@@ -842,7 +806,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_byUnderlyingPlayerAfterPrepare_notifiesOnPlaylistChanged()
throws Exception {
List playlistToSessionPlayer = TestUtils.createPlaylist(2);
@@ -862,7 +825,7 @@ public class SessionPlayerConnectorTest {
SessionPlayer player,
@Nullable List list,
@Nullable MediaMetadata metadata) {
- if (ObjectsCompat.equals(list, playlistToExoPlayer)) {
+ if (Util.areEqual(list, playlistToExoPlayer)) {
onPlaylistChangedLatch.countDown();
}
}
@@ -876,7 +839,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void addPlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List playlist = TestUtils.createPlaylist(10);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null));
@@ -905,7 +867,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void removePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List playlist = TestUtils.createPlaylist(10);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null));
@@ -933,7 +894,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void movePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List playlist = new ArrayList<>();
playlist.add(TestUtils.createMediaItem(R.raw.video_1));
@@ -967,7 +927,6 @@ public class SessionPlayerConnectorTest {
@Ignore
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void replacePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List playlist = TestUtils.createPlaylist(10);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null));
@@ -996,7 +955,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_withPlaylist_notifiesOnCurrentMediaItemChanged() throws Exception {
int listSize = 2;
List playlist = TestUtils.createPlaylist(listSize);
@@ -1011,7 +969,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_twice_finishes() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
@@ -1021,7 +978,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_withPlaylist_notifiesOnCurrentMediaItemChangedAndOnPlaybackCompleted()
throws Exception {
List playlist = new ArrayList<>();
@@ -1060,7 +1016,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void play_byUnderlyingPlayer_notifiesOnPlayerStateChanges() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
@@ -1086,7 +1041,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void pause_twice_finishes() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
@@ -1097,7 +1051,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void pause_byUnderlyingPlayer_notifiesOnPlayerStateChanges() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
@@ -1124,7 +1077,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void pause_byUnderlyingPlayerInListener_changesToPlayerStatePaused() throws Exception {
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
@@ -1169,7 +1121,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void skipToNextAndPrevious_calledInARow_notifiesOnCurrentMediaItemChanged()
throws Exception {
List playlist = new ArrayList<>();
@@ -1221,7 +1172,6 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setRepeatMode_withRepeatAll_continuesToPlayPlaylistWithoutBeingCompleted()
throws Exception {
List playlist = new ArrayList<>();
diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java
index e6d4550d88..9ff1f3dd24 100644
--- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java
+++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java
@@ -84,7 +84,7 @@ public class DefaultMediaItemConverter implements MediaItemConverter {
return new MediaItem.Builder()
.setUri(uri)
- .setMediaId(mediaId)
+ .setMediaId(mediaId != null ? mediaId : MediaItem.DEFAULT_MEDIA_ID)
.setMediaMetadata(
new com.google.android.exoplayer2.MediaMetadata.Builder().setTitle(title).build())
.setTag(media2MediaItem)
@@ -123,14 +123,14 @@ public class DefaultMediaItemConverter implements MediaItemConverter {
* MediaItem ExoPlayer MediaItem}.
*/
protected androidx.media2.common.MediaMetadata getMetadata(MediaItem exoPlayerMediaItem) {
- @Nullable String title = exoPlayerMediaItem.mediaMetadata.title;
+ @Nullable CharSequence title = exoPlayerMediaItem.mediaMetadata.title;
androidx.media2.common.MediaMetadata.Builder metadataBuilder =
new androidx.media2.common.MediaMetadata.Builder()
.putString(METADATA_KEY_MEDIA_ID, exoPlayerMediaItem.mediaId);
if (title != null) {
- metadataBuilder.putString(METADATA_KEY_TITLE, title);
- metadataBuilder.putString(METADATA_KEY_DISPLAY_TITLE, title);
+ metadataBuilder.putString(METADATA_KEY_TITLE, title.toString());
+ metadataBuilder.putString(METADATA_KEY_DISPLAY_TITLE, title.toString());
}
return metadataBuilder.build();
}
diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerCommandQueue.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerCommandQueue.java
index a1d4941f50..c65431a857 100644
--- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerCommandQueue.java
+++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerCommandQueue.java
@@ -39,7 +39,7 @@ import java.util.List;
import java.util.concurrent.Callable;
/** Manages the queue of player actions and handles running them one by one. */
-/* package */ class PlayerCommandQueue implements AutoCloseable {
+/* package */ class PlayerCommandQueue {
private static final String TAG = "PlayerCommandQueue";
private static final boolean DEBUG = false;
@@ -141,9 +141,6 @@ import java.util.concurrent.Callable;
@GuardedBy("lock")
private final Deque pendingPlayerCommandQueue;
- @GuardedBy("lock")
- private boolean closed;
-
// Should be only used on the handler.
@Nullable private AsyncPlayerCommandResult pendingAsyncPlayerCommandResult;
@@ -154,17 +151,6 @@ import java.util.concurrent.Callable;
pendingPlayerCommandQueue = new ArrayDeque<>();
}
- @Override
- public void close() {
- synchronized (lock) {
- if (closed) {
- return;
- }
- closed = true;
- }
- reset();
- }
-
public void reset() {
handler.removeCallbacksAndMessages(/* token= */ null);
List queue;
@@ -187,11 +173,6 @@ import java.util.concurrent.Callable;
@CommandCode int commandCode, Callable command, @Nullable Object tag) {
SettableFuture result = SettableFuture.create();
synchronized (lock) {
- if (closed) {
- // OK to set result with lock hold because developers cannot add listener here.
- result.set(new PlayerResult(PlayerResult.RESULT_ERROR_INVALID_STATE, /* item= */ null));
- return result;
- }
PlayerCommand playerCommand = new PlayerCommand(commandCode, command, result, tag);
result.addListener(
() -> {
diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java
index 74d7bd110e..f4d934bcf9 100644
--- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java
+++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java
@@ -20,7 +20,6 @@ import static com.google.android.exoplayer2.util.Util.postOrRun;
import android.os.Handler;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
-import androidx.core.util.ObjectsCompat;
import androidx.media.AudioAttributesCompat;
import androidx.media2.common.CallbackMediaItem;
import androidx.media2.common.MediaMetadata;
@@ -34,7 +33,6 @@ import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioAttributes;
-import com.google.android.exoplayer2.audio.AudioListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
@@ -139,10 +137,6 @@ import java.util.List;
controlDispatcher = new DefaultControlDispatcher();
componentListener = new ComponentListener();
player.addListener(componentListener);
- @Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
- if (audioComponent != null) {
- audioComponent.addAudioListener(componentListener);
- }
handler = new Handler(player.getApplicationLooper());
pollBufferRunnable = new PollBufferRunnable();
@@ -438,7 +432,7 @@ import java.util.List;
case Player.STATE_READY:
if (!prepared) {
prepared = true;
- handlePositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
+ handlePositionDiscontinuity(Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
listener.onPrepared(
Assertions.checkNotNull(getCurrentMediaItem()), player.getBufferedPercentage());
}
@@ -456,15 +450,15 @@ import java.util.List;
}
public void setAudioAttributes(AudioAttributesCompat audioAttributes) {
- Player.AudioComponent audioComponent = Assertions.checkStateNotNull(player.getAudioComponent());
- audioComponent.setAudioAttributes(
- Utils.getAudioAttributes(audioAttributes), /* handleAudioFocus= */ true);
+ // Player interface doesn't support setting audio attributes.
}
public AudioAttributesCompat getAudioAttributes() {
- @Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
- return Utils.getAudioAttributesCompat(
- audioComponent != null ? audioComponent.getAudioAttributes() : AudioAttributes.DEFAULT);
+ AudioAttributes audioAttributes = AudioAttributes.DEFAULT;
+ if (player.isCommandAvailable(Player.COMMAND_GET_AUDIO_ATTRIBUTES)) {
+ audioAttributes = player.getAudioAttributes();
+ }
+ return Utils.getAudioAttributesCompat(audioAttributes);
}
public void setPlaybackSpeed(float playbackSpeed) {
@@ -484,11 +478,6 @@ import java.util.List;
public void close() {
handler.removeCallbacks(pollBufferRunnable);
player.removeListener(componentListener);
-
- @Nullable Player.AudioComponent audioComponent = player.getAudioComponent();
- if (audioComponent != null) {
- audioComponent.removeAudioListener(componentListener);
- }
}
public boolean isCurrentMediaItemSeekable() {
@@ -518,9 +507,11 @@ import java.util.List;
int currentWindowIndex = getCurrentMediaItemIndex();
if (this.currentWindowIndex != currentWindowIndex) {
this.currentWindowIndex = currentWindowIndex;
- androidx.media2.common.MediaItem currentMediaItem =
- Assertions.checkNotNull(getCurrentMediaItem());
- listener.onCurrentMediaItemChanged(currentMediaItem);
+ if (currentWindowIndex != C.INDEX_UNSET) {
+ androidx.media2.common.MediaItem currentMediaItem =
+ Assertions.checkNotNull(getCurrentMediaItem());
+ listener.onCurrentMediaItemChanged(currentMediaItem);
+ }
} else {
listener.onSeekCompleted();
}
@@ -535,7 +526,7 @@ import java.util.List;
int windowCount = timeline.getWindowCount();
for (int i = 0; i < windowCount; i++) {
timeline.getWindow(i, window);
- if (!ObjectsCompat.equals(exoPlayerPlaylist.get(i), window.mediaItem)) {
+ if (!Util.areEqual(exoPlayerPlaylist.get(i), window.mediaItem)) {
return true;
}
}
@@ -583,7 +574,7 @@ import java.util.List;
}
}
- private final class ComponentListener implements Player.EventListener, AudioListener {
+ private final class ComponentListener implements Player.Listener {
// Player.EventListener implementation.
@@ -598,7 +589,10 @@ import java.util.List;
}
@Override
- public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ public void onPositionDiscontinuity(
+ Player.PositionInfo oldPosition,
+ Player.PositionInfo newPosition,
+ @Player.DiscontinuityReason int reason) {
handlePositionDiscontinuity(reason);
}
diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java
index 9a3dc09b07..e51964b648 100644
--- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java
+++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java
@@ -22,7 +22,6 @@ import androidx.annotation.FloatRange;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
-import androidx.core.util.ObjectsCompat;
import androidx.core.util.Pair;
import androidx.media.AudioAttributesCompat;
import androidx.media2.common.CallbackMediaItem;
@@ -36,6 +35,7 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.util.Util;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.util.HashMap;
@@ -578,7 +578,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
MediaItem currentMediaItem = player.getCurrentMediaItem();
boolean notifyCurrentMediaItem =
- !ObjectsCompat.equals(this.currentMediaItem, currentMediaItem) && currentMediaItem != null;
+ !Util.areEqual(this.currentMediaItem, currentMediaItem) && currentMediaItem != null;
this.currentMediaItem = currentMediaItem;
long currentPosition = getCurrentPosition();
@@ -594,7 +594,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
private void notifySkipToCompletedOnHandler() {
MediaItem currentMediaItem = Assertions.checkNotNull(player.getCurrentMediaItem());
- if (ObjectsCompat.equals(this.currentMediaItem, currentMediaItem)) {
+ if (Util.areEqual(this.currentMediaItem, currentMediaItem)) {
return;
}
this.currentMediaItem = currentMediaItem;
@@ -714,7 +714,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
@Override
public void onCurrentMediaItemChanged(MediaItem mediaItem) {
- if (ObjectsCompat.equals(currentMediaItem, mediaItem)) {
+ if (Util.areEqual(currentMediaItem, mediaItem)) {
return;
}
currentMediaItem = mediaItem;
diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle
index 5c827084da..9b812911ab 100644
--- a/extensions/mediasession/build.gradle
+++ b/extensions/mediasession/build.gradle
@@ -14,7 +14,7 @@
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
- implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-common')
api 'androidx.media:media:' + androidxMediaVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
index 179a8a3f11..f806a579be 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.mediasession;
+import static androidx.media.utils.MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID;
import static com.google.android.exoplayer2.Player.EVENT_IS_PLAYING_CHANGED;
import static com.google.android.exoplayer2.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED;
import static com.google.android.exoplayer2.Player.EVENT_PLAYBACK_STATE_CHANGED;
@@ -45,11 +46,11 @@ import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
-import com.google.android.exoplayer2.util.RepeatModeUtil;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -784,6 +785,10 @@ public final class MediaSessionConnector {
float playbackSpeed = player.getPlaybackParameters().speed;
extras.putFloat(EXTRAS_SPEED, playbackSpeed);
float sessionPlaybackSpeed = player.isPlaying() ? playbackSpeed : 0f;
+ @Nullable MediaItem currentMediaItem = player.getCurrentMediaItem();
+ if (currentMediaItem != null && !MediaItem.DEFAULT_MEDIA_ID.equals(currentMediaItem.mediaId)) {
+ extras.putString(PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID, currentMediaItem.mediaId);
+ }
builder
.setActions(buildPrepareActions() | buildPlaybackActions(player))
.setActiveQueueItemId(activeQueueItemId)
@@ -1080,13 +1085,12 @@ public final class MediaSessionConnector {
}
}
- private class ComponentListener extends MediaSessionCompat.Callback
- implements Player.EventListener {
+ private class ComponentListener extends MediaSessionCompat.Callback implements Player.Listener {
private int currentWindowIndex;
private int currentWindowCount;
- // Player.EventListener implementation.
+ // Player.Listener implementation.
@Override
public void onEvents(Player player, Player.Events events) {
@@ -1220,7 +1224,7 @@ public final class MediaSessionConnector {
@Override
public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int mediaSessionRepeatMode) {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) {
- @RepeatModeUtil.RepeatToggleModes int repeatMode;
+ @Player.RepeatMode int repeatMode;
switch (mediaSessionRepeatMode) {
case PlaybackStateCompat.REPEAT_MODE_ALL:
case PlaybackStateCompat.REPEAT_MODE_GROUP:
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
index 7f60d5e715..cab16744b9 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
@@ -23,15 +23,13 @@ import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
+import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
-import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
/**
- * A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link
- * ConcatenatingMediaSource}.
+ * A {@link MediaSessionConnector.QueueEditor} implementation.
*
*
This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
* the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it.
@@ -44,18 +42,17 @@ public final class TimelineQueueEditor
public static final String EXTRA_FROM_INDEX = "from_index";
public static final String EXTRA_TO_INDEX = "to_index";
- /**
- * Factory to create {@link MediaSource}s.
- */
- public interface MediaSourceFactory {
+ /** Converts a {@link MediaDescriptionCompat} to a {@link MediaItem}. */
+ public interface MediaDescriptionConverter {
/**
- * Creates a {@link MediaSource} for the given {@link MediaDescriptionCompat}.
+ * Returns a {@link MediaItem} for the given {@link MediaDescriptionCompat} or null if the
+ * description can't be converted.
*
- * @param description The {@link MediaDescriptionCompat} to create a media source for.
- * @return A {@link MediaSource} or {@code null} if no source can be created for the given
- * description.
+ *
If not null, the media item that is returned will be used to call {@link
+ * Player#addMediaItem(MediaItem)}.
*/
- @Nullable MediaSource createMediaSource(MediaDescriptionCompat description);
+ @Nullable
+ MediaItem convert(MediaDescriptionCompat description);
}
/**
@@ -110,51 +107,46 @@ public final class TimelineQueueEditor
public boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2) {
return Util.areEqual(d1.getMediaId(), d2.getMediaId());
}
-
}
private final MediaControllerCompat mediaController;
private final QueueDataAdapter queueDataAdapter;
- private final MediaSourceFactory sourceFactory;
+ private final MediaDescriptionConverter mediaDescriptionConverter;
private final MediaDescriptionEqualityChecker equalityChecker;
- private final ConcatenatingMediaSource queueMediaSource;
/**
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
*
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
- * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
- * @param sourceFactory The {@link MediaSourceFactory} to build media sources.
+ * @param mediaDescriptionConverter The {@link MediaDescriptionConverter} for converting media
+ * descriptions to {@link MediaItem MediaItems}.
*/
public TimelineQueueEditor(
MediaControllerCompat mediaController,
- ConcatenatingMediaSource queueMediaSource,
QueueDataAdapter queueDataAdapter,
- MediaSourceFactory sourceFactory) {
- this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
- new MediaIdEqualityChecker());
+ MediaDescriptionConverter mediaDescriptionConverter) {
+ this(
+ mediaController, queueDataAdapter, mediaDescriptionConverter, new MediaIdEqualityChecker());
}
/**
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
*
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
- * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
- * @param sourceFactory The {@link MediaSourceFactory} to build media sources.
+ * @param mediaDescriptionConverter The {@link MediaDescriptionConverter} for converting media
+ * descriptions to {@link MediaItem MediaItems}.
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
*/
public TimelineQueueEditor(
MediaControllerCompat mediaController,
- ConcatenatingMediaSource queueMediaSource,
QueueDataAdapter queueDataAdapter,
- MediaSourceFactory sourceFactory,
+ MediaDescriptionConverter mediaDescriptionConverter,
MediaDescriptionEqualityChecker equalityChecker) {
this.mediaController = mediaController;
- this.queueMediaSource = queueMediaSource;
this.queueDataAdapter = queueDataAdapter;
- this.sourceFactory = sourceFactory;
+ this.mediaDescriptionConverter = mediaDescriptionConverter;
this.equalityChecker = equalityChecker;
}
@@ -165,10 +157,10 @@ public final class TimelineQueueEditor
@Override
public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {
- @Nullable MediaSource mediaSource = sourceFactory.createMediaSource(description);
- if (mediaSource != null) {
+ @Nullable MediaItem mediaItem = mediaDescriptionConverter.convert(description);
+ if (mediaItem != null) {
queueDataAdapter.add(index, description);
- queueMediaSource.addMediaSource(index, mediaSource);
+ player.addMediaItem(index, mediaItem);
}
}
@@ -178,7 +170,7 @@ public final class TimelineQueueEditor
for (int i = 0; i < queue.size(); i++) {
if (equalityChecker.equals(queue.get(i).getDescription(), description)) {
queueDataAdapter.remove(i);
- queueMediaSource.removeMediaSource(i);
+ player.removeMediaItem(i);
return;
}
}
@@ -200,9 +192,8 @@ public final class TimelineQueueEditor
int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET);
if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) {
queueDataAdapter.move(from, to);
- queueMediaSource.moveMediaSource(from, to);
+ player.moveMediaItem(from, to);
}
return true;
}
-
}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
index bc86da4a86..4a27ca9d93 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
@@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer2.ext.mediasession;
+import static com.google.android.exoplayer2.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
+import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
+import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static java.lang.Math.min;
import android.os.Bundle;
@@ -98,8 +101,13 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
if (!timeline.isEmpty() && !player.isPlayingAd()) {
timeline.getWindow(player.getCurrentWindowIndex(), window);
enableSkipTo = timeline.getWindowCount() > 1;
- enablePrevious = window.isSeekable || !window.isLive() || player.hasPrevious();
- enableNext = (window.isLive() && window.isDynamic) || player.hasNext();
+ enablePrevious =
+ player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)
+ || !window.isLive()
+ || player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
+ enableNext =
+ (window.isLive() && window.isDynamic)
+ || player.isCommandAvailable(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
}
long actions = 0;
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
index d23dd22574..0b881955a1 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.okhttp;
+import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.min;
@@ -27,11 +28,13 @@ 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.HttpDataSource;
+import com.google.android.exoplayer2.upstream.HttpUtil;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
+import com.google.common.base.Ascii;
import com.google.common.base.Predicate;
-import java.io.EOFException;
+import com.google.common.net.HttpHeaders;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
@@ -168,8 +171,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
}
}
- private static final byte[] SKIP_BUFFER = new byte[4096];
-
private final Call.Factory callFactory;
private final RequestProperties requestProperties;
@@ -182,11 +183,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
@Nullable private Response response;
@Nullable private InputStream responseByteStream;
private boolean opened;
-
- private long bytesToSkip;
private long bytesToRead;
-
- private long bytesSkipped;
private long bytesRead;
/** @deprecated Use {@link OkHttpDataSource.Factory} instead. */
@@ -278,8 +275,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {
this.dataSpec = dataSpec;
- this.bytesRead = 0;
- this.bytesSkipped = 0;
+ bytesRead = 0;
+ bytesToRead = 0;
transferInitializing(dataSpec);
Request request = makeRequest(dataSpec);
@@ -293,7 +290,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
} catch (IOException e) {
@Nullable String message = e.getMessage();
if (message != null
- && Util.toLowerInvariant(message).matches("cleartext communication.*not permitted.*")) {
+ && Ascii.toLowerCase(message).matches("cleartext communication.*not permitted.*")) {
throw new CleartextNotPermittedException(e, dataSpec);
}
throw new HttpDataSourceException(
@@ -304,6 +301,16 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
// Check for a valid response code.
if (!response.isSuccessful()) {
+ if (responseCode == 416) {
+ long documentSize =
+ HttpUtil.getDocumentSize(response.headers().get(HttpHeaders.CONTENT_RANGE));
+ if (dataSpec.position == documentSize) {
+ opened = true;
+ transferStarted(dataSpec);
+ return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0;
+ }
+ }
+
byte[] errorResponseBody;
try {
errorResponseBody = Util.toByteArray(Assertions.checkNotNull(responseByteStream));
@@ -332,7 +339,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
// If we requested a range starting from a non-zero position and received a 200 rather than a
// 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
- bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
+ long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Determine the length of the data to be read, after skipping.
if (dataSpec.length != C.LENGTH_UNSET) {
@@ -345,13 +352,21 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
opened = true;
transferStarted(dataSpec);
+ try {
+ if (!skipFully(bytesToSkip)) {
+ throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
+ }
+ } catch (IOException e) {
+ closeConnectionQuietly();
+ throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
return bytesToRead;
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
try {
- skipInternal();
return readInternal(buffer, offset, readLength);
} catch (IOException e) {
throw new HttpDataSourceException(
@@ -368,38 +383,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
}
}
- /**
- * Returns the number of bytes that have been skipped since the most recent call to
- * {@link #open(DataSpec)}.
- *
- * @return The number of bytes skipped.
- */
- protected final long bytesSkipped() {
- return bytesSkipped;
- }
-
- /**
- * Returns the number of bytes that have been read since the most recent call to
- * {@link #open(DataSpec)}.
- *
- * @return The number of bytes read.
- */
- protected final long bytesRead() {
- return bytesRead;
- }
-
- /**
- * Returns the number of bytes that are still to be read for the current {@link DataSpec}.
- *
- * If the total length of the data being read is known, then this length minus {@code bytesRead()}
- * is returned. If the total length is unknown, {@link C#LENGTH_UNSET} is returned.
- *
- * @return The remaining length, or {@link C#LENGTH_UNSET}.
- */
- protected final long bytesRemaining() {
- return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;
- }
-
/** Establishes a connection. */
private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
long position = dataSpec.position;
@@ -428,18 +411,15 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
builder.header(header.getKey(), header.getValue());
}
- if (!(position == 0 && length == C.LENGTH_UNSET)) {
- String rangeRequest = "bytes=" + position + "-";
- if (length != C.LENGTH_UNSET) {
- rangeRequest += (position + length - 1);
- }
- builder.addHeader("Range", rangeRequest);
+ @Nullable String rangeHeader = buildRangeRequestHeader(position, length);
+ if (rangeHeader != null) {
+ builder.addHeader(HttpHeaders.RANGE, rangeHeader);
}
if (userAgent != null) {
- builder.addHeader("User-Agent", userAgent);
+ builder.addHeader(HttpHeaders.USER_AGENT, userAgent);
}
if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
- builder.addHeader("Accept-Encoding", "identity");
+ builder.addHeader(HttpHeaders.ACCEPT_ENCODING, "identity");
}
@Nullable RequestBody requestBody = null;
@@ -454,30 +434,32 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
}
/**
- * Skips any bytes that need skipping. Else does nothing.
- *
- * This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
+ * Attempts to skip the specified number of bytes in full.
*
+ * @param bytesToSkip The number of bytes to skip.
* @throws InterruptedIOException If the thread is interrupted during the operation.
- * @throws EOFException If the end of the input stream is reached before the bytes are skipped.
+ * @throws IOException If an error occurs reading from the source.
+ * @return Whether the bytes were skipped in full. If {@code false} then the data ended before the
+ * specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}.
*/
- private void skipInternal() throws IOException {
- if (bytesSkipped == bytesToSkip) {
- return;
+ private boolean skipFully(long bytesToSkip) throws IOException {
+ if (bytesToSkip == 0) {
+ return true;
}
-
- while (bytesSkipped != bytesToSkip) {
- int readLength = (int) min(bytesToSkip - bytesSkipped, SKIP_BUFFER.length);
- int read = castNonNull(responseByteStream).read(SKIP_BUFFER, 0, readLength);
+ byte[] skipBuffer = new byte[4096];
+ while (bytesToSkip > 0) {
+ int readLength = (int) min(bytesToSkip, skipBuffer.length);
+ int read = castNonNull(responseByteStream).read(skipBuffer, 0, readLength);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException();
}
if (read == -1) {
- throw new EOFException();
+ return false;
}
- bytesSkipped += read;
+ bytesToSkip -= read;
bytesTransferred(read);
}
+ return true;
}
/**
@@ -508,10 +490,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
int read = castNonNull(responseByteStream).read(buffer, offset, readLength);
if (read == -1) {
- if (bytesToRead != C.LENGTH_UNSET) {
- // End of stream reached having not read sufficient data.
- throw new EOFException();
- }
return C.RESULT_END_OF_INPUT;
}
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
index 08e337f52b..5b6a31ca92 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.ext.okhttp;
-
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
index c964b0cc1c..9cb606a718 100644
--- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
+++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
@@ -24,9 +24,11 @@ import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Renderer;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
@@ -71,12 +73,12 @@ public class OpusPlaybackTest {
}
}
- private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
+ private static class TestPlaybackRunnable implements Player.Listener, Runnable {
private final Context context;
private final Uri uri;
- @Nullable private ExoPlayer player;
+ @Nullable private SimpleExoPlayer player;
@Nullable private ExoPlaybackException playbackException;
public TestPlaybackRunnable(Uri uri, Context context) {
@@ -87,8 +89,14 @@ public class OpusPlaybackTest {
@Override
public void run() {
Looper.prepare();
- LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer();
- player = new ExoPlayer.Builder(context, audioRenderer).build();
+ RenderersFactory renderersFactory =
+ (eventHandler,
+ videoRendererEventListener,
+ audioRendererEventListener,
+ textRendererOutput,
+ metadataRendererOutput) ->
+ new Renderer[] {new LibopusAudioRenderer(eventHandler, audioRendererEventListener)};
+ player = new SimpleExoPlayer.Builder(context, renderersFactory).build();
player.addListener(this);
MediaSource mediaSource =
new ProgressiveMediaSource.Factory(
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
index 5529701c06..71ba1db106 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
@@ -21,9 +21,7 @@ import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.Util;
-/**
- * Configures and queries the underlying native library.
- */
+/** Configures and queries the underlying native library. */
public final class OpusLibrary {
static {
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
index db60eea269..167a4175d7 100644
--- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
@@ -20,9 +20,7 @@ import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener;
-/**
- * A {@link Factory} that produces {@link RtmpDataSource}.
- */
+/** A {@link Factory} that produces {@link RtmpDataSource}. */
public final class RtmpDataSourceFactory implements DataSource.Factory {
@Nullable private final TransferListener listener;
diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
index 823ce02cfe..d7a19f1662 100644
--- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
+++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
@@ -25,10 +25,11 @@ import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
@@ -100,12 +101,12 @@ public class VpxPlaybackTest {
}
}
- private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
+ private static class TestPlaybackRunnable implements Player.Listener, Runnable {
private final Context context;
private final Uri uri;
- @Nullable private ExoPlayer player;
+ @Nullable private SimpleExoPlayer player;
@Nullable private ExoPlaybackException playbackException;
public TestPlaybackRunnable(Uri uri, Context context) {
@@ -116,18 +117,26 @@ public class VpxPlaybackTest {
@Override
public void run() {
Looper.prepare();
- LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0);
- player = new ExoPlayer.Builder(context, videoRenderer).build();
+ RenderersFactory renderersFactory =
+ (eventHandler,
+ videoRendererEventListener,
+ audioRendererEventListener,
+ textRendererOutput,
+ metadataRendererOutput) ->
+ new Renderer[] {
+ new LibvpxVideoRenderer(
+ /* allowedJoiningTimeMs= */ 0,
+ eventHandler,
+ videoRendererEventListener,
+ /* maxDroppedFramesToNotify= */ -1)
+ };
+ player = new SimpleExoPlayer.Builder(context, renderersFactory).build();
player.addListener(this);
MediaSource mediaSource =
new ProgressiveMediaSource.Factory(
new DefaultDataSourceFactory(context), MatroskaExtractor.FACTORY)
.createMediaSource(MediaItem.fromUri(uri));
- player
- .createMessage(videoRenderer)
- .setType(Renderer.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER)
- .setPayload(new VideoDecoderGLSurfaceView(context).getVideoDecoderOutputBufferRenderer())
- .send();
+ player.setVideoSurfaceView(new VideoDecoderGLSurfaceView(context));
player.setMediaSource(mediaSource);
player.prepare();
player.play();
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
index 5106ab67ad..339ec021c6 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
@@ -21,9 +21,7 @@ import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.Util;
-/**
- * Configures and queries the underlying native library.
- */
+/** Configures and queries the underlying native library. */
public final class VpxLibrary {
static {
diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle
index b3624e75dc..02a1055d87 100644
--- a/extensions/workmanager/build.gradle
+++ b/extensions/workmanager/build.gradle
@@ -17,7 +17,7 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'androidx.work:work-runtime:2.4.0'
+ implementation 'androidx.work:work-runtime:2.5.0'
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index eefcdc910f..8efe8c9f90 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
+distributionUrl=https://services.gradle.org/distributions/gradle-6.7.1-all.zip
diff --git a/javadoc_library.gradle b/javadoc_library.gradle
index bb17dcb035..20be99ee12 100644
--- a/javadoc_library.gradle
+++ b/javadoc_library.gradle
@@ -25,7 +25,7 @@ android.libraryVariants.all { variant ->
task("generateJavadoc", type: Javadoc) {
description = "Generates Javadoc for the ${javadocTitle}."
title = "ExoPlayer ${javadocTitle}"
- source = allSourceDirs
+ source = allSourceDirs + "${buildDir}/generated/aidl_source_output_dir/"
options {
links "https://developer.android.com/reference",
"https://guava.dev/releases/$project.ext.guavaVersion/api/docs"
diff --git a/library/all/build.gradle b/library/all/build.gradle
index e18d856c83..56414c14bf 100644
--- a/library/all/build.gradle
+++ b/library/all/build.gradle
@@ -17,6 +17,7 @@ dependencies {
api project(modulePrefix + 'library-core')
api project(modulePrefix + 'library-dash')
api project(modulePrefix + 'library-hls')
+ api project(modulePrefix + 'library-rtsp')
api project(modulePrefix + 'library-smoothstreaming')
api project(modulePrefix + 'library-transformer')
api project(modulePrefix + 'library-ui')
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java
index d99a320e32..57550d589b 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java
@@ -30,47 +30,70 @@ public abstract class BasePlayer implements Player {
}
@Override
- public void setMediaItem(MediaItem mediaItem) {
+ public final void setMediaItem(MediaItem mediaItem) {
setMediaItems(Collections.singletonList(mediaItem));
}
@Override
- public void setMediaItem(MediaItem mediaItem, long startPositionMs) {
+ public final void setMediaItem(MediaItem mediaItem, long startPositionMs) {
setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs);
}
@Override
- public void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
+ public final void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
setMediaItems(Collections.singletonList(mediaItem), resetPosition);
}
@Override
- public void setMediaItems(List mediaItems) {
+ public final void setMediaItems(List mediaItems) {
setMediaItems(mediaItems, /* resetPosition= */ true);
}
@Override
- public void addMediaItem(int index, MediaItem mediaItem) {
+ public final void addMediaItem(int index, MediaItem mediaItem) {
addMediaItems(index, Collections.singletonList(mediaItem));
}
@Override
- public void addMediaItem(MediaItem mediaItem) {
+ public final void addMediaItem(MediaItem mediaItem) {
addMediaItems(Collections.singletonList(mediaItem));
}
@Override
- public void moveMediaItem(int currentIndex, int newIndex) {
+ public final void addMediaItems(List mediaItems) {
+ addMediaItems(/* index= */ Integer.MAX_VALUE, mediaItems);
+ }
+
+ @Override
+ public final void moveMediaItem(int currentIndex, int newIndex) {
if (currentIndex != newIndex) {
moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex);
}
}
@Override
- public void removeMediaItem(int index) {
+ public final void removeMediaItem(int index) {
removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1);
}
+ @Override
+ public final void clearMediaItems() {
+ removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ Integer.MAX_VALUE);
+ }
+
+ @Override
+ public final boolean isCommandAvailable(@Command int command) {
+ return getAvailableCommands().contains(command);
+ }
+
+ /** @deprecated Use {@link #getPlayerError()} instead. */
+ @Deprecated
+ @Override
+ @Nullable
+ public final ExoPlaybackException getPlaybackError() {
+ return getPlayerError();
+ }
+
@Override
public final void play() {
setPlayWhenReady(true);
@@ -129,6 +152,11 @@ public abstract class BasePlayer implements Player {
}
}
+ @Override
+ public final void setPlaybackSpeed(float speed) {
+ setPlaybackParameters(getPlaybackParameters().withSpeed(speed));
+ }
+
@Override
public final void stop() {
stop(/* reset= */ false);
@@ -180,12 +208,12 @@ public abstract class BasePlayer implements Player {
}
@Override
- public int getMediaItemCount() {
+ public final int getMediaItemCount() {
return getCurrentTimeline().getWindowCount();
}
@Override
- public MediaItem getMediaItemAt(int index) {
+ public final MediaItem getMediaItemAt(int index) {
return getCurrentTimeline().getWindow(index, window).mediaItem;
}
@@ -249,4 +277,15 @@ public abstract class BasePlayer implements Player {
@RepeatMode int repeatMode = getRepeatMode();
return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode;
}
+
+ protected Commands getAvailableCommands(Commands permanentAvailableCommands) {
+ return new Commands.Builder()
+ .addAll(permanentAvailableCommands)
+ .addIf(COMMAND_SEEK_TO_DEFAULT_POSITION, !isPlayingAd())
+ .addIf(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, isCurrentWindowSeekable() && !isPlayingAd())
+ .addIf(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNext() && !isPlayingAd())
+ .addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPrevious() && !isPlayingAd())
+ .addIf(COMMAND_SEEK_TO_MEDIA_ITEM, !isPlayingAd())
+ .build();
+ }
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/BundleListRetriever.java b/library/common/src/main/java/com/google/android/exoplayer2/BundleListRetriever.java
new file mode 100644
index 0000000000..4deaf43a8f
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/BundleListRetriever.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 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;
+
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.RemoteException;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.util.Util;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+/**
+ * A {@link Binder} to transfer a list of {@link Bundle Bundles} across processes by splitting the
+ * list into multiple transactions.
+ *
+ *
Note: Using this class causes synchronous binder calls in the opposite direction regardless of
+ * the "oneway" property.
+ *
+ *
Example usage:
+ *
+ *
{@code
+ * // Sender
+ * List list = ...;
+ * IBinder binder = new BundleListRetriever(list);
+ * Bundle bundle = new Bundle();
+ * bundle.putBinder("list", binder);
+ *
+ * // Receiver
+ * Bundle bundle = ...; // Received from the sender
+ * IBinder binder = bundle.getBinder("list");
+ * List list = BundleListRetriever.getList(binder);
+ * }
+ */
+public final class BundleListRetriever extends Binder {
+
+ // Soft limit of an IPC buffer size
+ private static final int SUGGESTED_MAX_IPC_SIZE =
+ Util.SDK_INT >= 30 ? IBinder.getSuggestedMaxIpcSizeBytes() : 64 * 1024;
+
+ private static final int REPLY_END_OF_LIST = 0;
+ private static final int REPLY_CONTINUE = 1;
+ private static final int REPLY_BREAK = 2;
+
+ private final ImmutableList list;
+
+ /** Creates a {@link Binder} to send a list of {@link Bundle Bundles} to another process. */
+ public BundleListRetriever(List list) {
+ this.list = ImmutableList.copyOf(list);
+ }
+
+ @Override
+ protected boolean onTransact(int code, Parcel data, @Nullable Parcel reply, int flags)
+ throws RemoteException {
+ if (code != FIRST_CALL_TRANSACTION) {
+ return super.onTransact(code, data, reply, flags);
+ }
+
+ if (reply == null) {
+ return false;
+ }
+
+ int count = list.size();
+ int index = data.readInt();
+ while (index < count && reply.dataSize() < SUGGESTED_MAX_IPC_SIZE) {
+ reply.writeInt(REPLY_CONTINUE);
+ reply.writeBundle(list.get(index));
+ index++;
+ }
+ reply.writeInt(index < count ? REPLY_BREAK : REPLY_END_OF_LIST);
+ return true;
+ }
+
+ /**
+ * Gets a list of {@link Bundle Bundles} from a {@link BundleListRetriever}.
+ *
+ * @param binder A binder interface backed by {@link BundleListRetriever}.
+ * @return The list of {@link Bundle Bundles}.
+ */
+ public static ImmutableList getList(IBinder binder) {
+ ImmutableList.Builder builder = ImmutableList.builder();
+
+ int index = 0;
+ int replyCode = REPLY_CONTINUE;
+
+ while (replyCode != REPLY_END_OF_LIST) {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ try {
+ data.writeInt(index);
+ try {
+ binder.transact(FIRST_CALL_TRANSACTION, data, reply, /* flags= */ 0);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ while ((replyCode = reply.readInt()) == REPLY_CONTINUE) {
+ builder.add(checkNotNull(reply.readBundle()));
+ index++;
+ }
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+
+ return builder.build();
+ }
+}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Bundleable.java b/library/common/src/main/java/com/google/android/exoplayer2/Bundleable.java
new file mode 100644
index 0000000000..29dae2e50e
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/Bundleable.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 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;
+
+import android.os.Bundle;
+
+/**
+ * Interface for classes whose instance can be stored in a {@link Bundle} by {@link #toBundle()} and
+ * can be restored from the {@link Bundle} by using the static {@code CREATOR} field that implements
+ * {@link Bundleable.Creator}.
+ *
+ *
For example, a {@link Bundleable} class {@code Foo} supports the following:
+ *
+ *
+ */
+public interface Bundleable {
+
+ /** Returns a {@link Bundle} representing the information stored in this object. */
+ Bundle toBundle();
+
+ /** Interface for the static {@code CREATOR} field of {@link Bundleable} classes. */
+ interface Creator {
+
+ /**
+ * Restores a {@link Bundleable} instance from a {@link Bundle} produced by {@link
+ * Bundleable#toBundle()}.
+ *
+ *
It guarantees the compatibility of {@link Bundle} representations produced by different
+ * versions of {@link Bundleable#toBundle()} by providing best default values for missing
+ * fields. It throws an exception if any essential fields are missing.
+ */
+ T fromBundle(Bundle bundle);
+ }
+}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/C.java b/library/common/src/main/java/com/google/android/exoplayer2/C.java
index 1c2cc92362..eba6c0cfcd 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/C.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/C.java
@@ -31,9 +31,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.UUID;
-/**
- * Defines constants used by the library.
- */
+/** Defines constants used by the library. */
@SuppressWarnings("InlinedApi")
public final class C {
@@ -553,8 +551,8 @@ public final class C {
/** Video decoder output mode that renders 4:2:0 YUV planes directly to a surface. */
public static final int VIDEO_OUTPUT_MODE_SURFACE_YUV = 1;
// LINT.ThenChange(
- // ../../../../../../../../../extensions/av1/src/main/jni/gav1_jni.cc,
- // ../../../../../../../../../extensions/vp9/src/main/jni/vpx_jni.cc
+ // ../../../../../../../../../../../media/libraries/decoder_av1/src/main/jni/gav1_jni.cc,
+ // ../../../../../../../../../../../media/libraries/decoder_vp9/src/main/jni/vpx_jni.cc
// )
/**
@@ -588,7 +586,15 @@ public final class C {
* Indicates that the track should be selected if user preferences do not state otherwise.
*/
public static final int SELECTION_FLAG_DEFAULT = 1;
- /** Indicates that the track must be displayed. Only applies to text tracks. */
+ /**
+ * Indicates that the track should be selected if its language matches the language of the
+ * selected audio track and user preferences do not state otherwise. Only applies to text tracks.
+ *
+ *
Tracks with this flag generally provide translation for elements that don't match the
+ * declared language of the selected audio track (e.g. speech in an alien language). See Netflix's summary
+ * for more info.
+ */
public static final int SELECTION_FLAG_FORCED = 1 << 1; // 2
/**
* Indicates that the player may choose to play the track in absence of an explicit user
@@ -601,11 +607,11 @@ public final class C {
/**
* Represents a streaming or other media type. One of {@link #TYPE_DASH}, {@link #TYPE_SS}, {@link
- * #TYPE_HLS} or {@link #TYPE_OTHER}.
+ * #TYPE_HLS}, {@link #TYPE_RTSP} or {@link #TYPE_OTHER}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
- @IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_OTHER})
+ @IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_RTSP, TYPE_OTHER})
public @interface ContentType {}
/**
* Value returned by {@link Util#inferContentType(String)} for DASH manifests.
@@ -619,11 +625,13 @@ public final class C {
* Value returned by {@link Util#inferContentType(String)} for HLS manifests.
*/
public static final int TYPE_HLS = 2;
+ /** Value returned by {@link Util#inferContentType(String)} for RTSP. */
+ public static final int TYPE_RTSP = 3;
/**
* Value returned by {@link Util#inferContentType(String)} for files other than DASH, HLS or
- * Smooth Streaming manifests.
+ * Smooth Streaming manifests, or RTSP URIs.
*/
- public static final int TYPE_OTHER = 3;
+ public static final int TYPE_OTHER = 4;
/**
* A return value for methods where the end of an input was encountered.
@@ -774,7 +782,7 @@ public final class C {
*/
public static final UUID PLAYREADY_UUID = new UUID(0x9A04F07998404286L, 0xAB92E65BE0885F95L);
- /** @deprecated Use {@code Renderer.MSG_SET_SURFACE}. */
+ /** @deprecated Use {@code Renderer.MSG_SET_VIDEO_OUTPUT}. */
@Deprecated public static final int MSG_SET_SURFACE = 1;
/** @deprecated Use {@code Renderer.MSG_SET_VOLUME}. */
@@ -795,9 +803,6 @@ public final class C {
/** @deprecated Use {@code Renderer.MSG_SET_CAMERA_MOTION_LISTENER}. */
@Deprecated public static final int MSG_SET_CAMERA_MOTION_LISTENER = 7;
- /** @deprecated Use {@code Renderer.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER}. */
- @Deprecated public static final int MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER = 8;
-
/** @deprecated Use {@code Renderer.MSG_CUSTOM_BASE}. */
@Deprecated public static final int MSG_CUSTOM_BASE = 10000;
@@ -930,8 +935,8 @@ public final class C {
/**
* Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE},
* {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link
- * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link
- * #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}.
+ * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G_SA}, {@link #NETWORK_TYPE_5G_NSA}, {@link
+ * #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@@ -942,7 +947,8 @@ public final class C {
NETWORK_TYPE_2G,
NETWORK_TYPE_3G,
NETWORK_TYPE_4G,
- NETWORK_TYPE_5G,
+ NETWORK_TYPE_5G_SA,
+ NETWORK_TYPE_5G_NSA,
NETWORK_TYPE_CELLULAR_UNKNOWN,
NETWORK_TYPE_ETHERNET,
NETWORK_TYPE_OTHER
@@ -960,8 +966,10 @@ public final class C {
public static final int NETWORK_TYPE_3G = 4;
/** Network type for a 4G cellular connection. */
public static final int NETWORK_TYPE_4G = 5;
- /** Network type for a 5G cellular connection. */
- public static final int NETWORK_TYPE_5G = 9;
+ /** Network type for a 5G stand-alone (SA) cellular connection. */
+ public static final int NETWORK_TYPE_5G_SA = 9;
+ /** Network type for a 5G non-stand-alone (NSA) cellular connection. */
+ public static final int NETWORK_TYPE_5G_NSA = 10;
/**
* Network type for cellular connections which cannot be mapped to one of {@link
* #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java b/library/common/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java
similarity index 97%
rename from library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java
rename to library/common/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java
index d3ec2cb9db..4e9b20acf3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java
@@ -19,8 +19,8 @@ import com.google.android.exoplayer2.Player.RepeatMode;
/**
* Dispatches operations to the {@link Player}.
- *
- * Implementations may choose to suppress (e.g. prevent playback from resuming if audio focus is
+ *
+ *
Implementations may choose to suppress (e.g. prevent playback from resuming if audio focus is
* denied) or modify (e.g. change the seek position to prevent a user from seeking past a
* non-skippable advert) operations.
*/
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java b/library/common/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java
similarity index 100%
rename from library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java
rename to library/common/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
index 95edfdf6f4..eb04eef9c6 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2;
+import android.os.Bundle;
+import android.os.RemoteException;
import android.os.SystemClock;
import android.text.TextUtils;
import androidx.annotation.CheckResult;
@@ -23,13 +25,14 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C.FormatSupport;
import com.google.android.exoplayer2.source.MediaPeriodId;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Thrown when a non locally recoverable playback failure occurs. */
-public final class ExoPlaybackException extends Exception {
+public final class ExoPlaybackException extends Exception implements Bundleable {
/**
* The type of source that produced the error. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER}
@@ -102,13 +105,9 @@ public final class ExoPlaybackException extends Exception {
@Nullable public final MediaPeriodId mediaPeriodId;
/**
- * Whether the error may be recoverable.
- *
- *
This is only used internally by ExoPlayer to try to recover from some errors and should not
- * be used by apps.
- *
- *
If the {@link #type} is {@link #TYPE_RENDERER}, it may be possible to recover from the error
- * by disabling and re-enabling the renderers.
+ * If {@link #type} is {@link #TYPE_RENDERER}, this field indicates whether the error may be
+ * recoverable by disabling and re-enabling (but not resetting) the renderers. For other
+ * {@link Type types} this field will always be {@code false}.
*/
/* package */ final boolean isRecoverable;
@@ -271,7 +270,7 @@ public final class ExoPlaybackException extends Exception {
}
private ExoPlaybackException(
- @Nullable String message,
+ String message,
@Nullable Throwable cause,
@Type int type,
@Nullable String rendererName,
@@ -282,6 +281,7 @@ public final class ExoPlaybackException extends Exception {
long timestampMs,
boolean isRecoverable) {
super(message, cause);
+ Assertions.checkArgument(!isRecoverable || type == TYPE_RENDERER);
this.type = type;
this.cause = cause;
this.rendererName = rendererName;
@@ -332,7 +332,7 @@ public final class ExoPlaybackException extends Exception {
@CheckResult
/* package */ ExoPlaybackException copyWithMediaPeriodId(@Nullable MediaPeriodId mediaPeriodId) {
return new ExoPlaybackException(
- getMessage(),
+ Util.castNonNull(getMessage()),
cause,
type,
rendererName,
@@ -344,7 +344,6 @@ public final class ExoPlaybackException extends Exception {
isRecoverable);
}
- @Nullable
private static String deriveMessage(
@Type int type,
@Nullable String customMessage,
@@ -352,7 +351,7 @@ public final class ExoPlaybackException extends Exception {
int rendererIndex,
@Nullable Format rendererFormat,
@FormatSupport int rendererFormatSupport) {
- @Nullable String message;
+ String message;
switch (type) {
case TYPE_SOURCE:
message = "Source error";
@@ -381,4 +380,136 @@ public final class ExoPlaybackException extends Exception {
}
return message;
}
+
+ // Bundleable implementation.
+ // TODO(b/145954241): Revisit bundling fields when this class is split for Player and ExoPlayer.
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_MESSAGE,
+ FIELD_TYPE,
+ FIELD_RENDERER_NAME,
+ FIELD_RENDERER_INDEX,
+ FIELD_RENDERER_FORMAT,
+ FIELD_RENDERER_FORMAT_SUPPORT,
+ FIELD_TIME_STAMP_MS,
+ FIELD_IS_RECOVERABLE,
+ FIELD_CAUSE_CLASS_NAME,
+ FIELD_CAUSE_MESSAGE
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_MESSAGE = 0;
+ private static final int FIELD_TYPE = 1;
+ private static final int FIELD_RENDERER_NAME = 2;
+ private static final int FIELD_RENDERER_INDEX = 3;
+ private static final int FIELD_RENDERER_FORMAT = 4;
+ private static final int FIELD_RENDERER_FORMAT_SUPPORT = 5;
+ private static final int FIELD_TIME_STAMP_MS = 6;
+ private static final int FIELD_IS_RECOVERABLE = 7;
+ private static final int FIELD_CAUSE_CLASS_NAME = 8;
+ private static final int FIELD_CAUSE_MESSAGE = 9;
+
+ /**
+ * {@inheritDoc}
+ *
+ *
It omits the {@link #mediaPeriodId} field. The {@link #mediaPeriodId} of an instance
+ * restored by {@link #CREATOR} will always be {@code null}.
+ */
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString(keyForField(FIELD_MESSAGE), getMessage());
+ bundle.putInt(keyForField(FIELD_TYPE), type);
+ bundle.putString(keyForField(FIELD_RENDERER_NAME), rendererName);
+ bundle.putInt(keyForField(FIELD_RENDERER_INDEX), rendererIndex);
+ bundle.putParcelable(keyForField(FIELD_RENDERER_FORMAT), rendererFormat);
+ bundle.putInt(keyForField(FIELD_RENDERER_FORMAT_SUPPORT), rendererFormatSupport);
+ bundle.putLong(keyForField(FIELD_TIME_STAMP_MS), timestampMs);
+ bundle.putBoolean(keyForField(FIELD_IS_RECOVERABLE), isRecoverable);
+ if (cause != null) {
+ bundle.putString(keyForField(FIELD_CAUSE_CLASS_NAME), cause.getClass().getName());
+ bundle.putString(keyForField(FIELD_CAUSE_MESSAGE), cause.getMessage());
+ }
+ return bundle;
+ }
+
+ /** Object that can restore {@link ExoPlaybackException} from a {@link Bundle}. */
+ public static final Creator CREATOR = ExoPlaybackException::fromBundle;
+
+ private static ExoPlaybackException fromBundle(Bundle bundle) {
+ int type = bundle.getInt(keyForField(FIELD_TYPE), /* defaultValue= */ TYPE_UNEXPECTED);
+ @Nullable String rendererName = bundle.getString(keyForField(FIELD_RENDERER_NAME));
+ int rendererIndex =
+ bundle.getInt(keyForField(FIELD_RENDERER_INDEX), /* defaultValue= */ C.INDEX_UNSET);
+ @Nullable Format rendererFormat = bundle.getParcelable(keyForField(FIELD_RENDERER_FORMAT));
+ int rendererFormatSupport =
+ bundle.getInt(
+ keyForField(FIELD_RENDERER_FORMAT_SUPPORT), /* defaultValue= */ C.FORMAT_HANDLED);
+ long timestampMs =
+ bundle.getLong(
+ keyForField(FIELD_TIME_STAMP_MS), /* defaultValue= */ SystemClock.elapsedRealtime());
+ boolean isRecoverable =
+ bundle.getBoolean(keyForField(FIELD_IS_RECOVERABLE), /* defaultValue= */ false);
+ @Nullable String message = bundle.getString(keyForField(FIELD_MESSAGE));
+ if (message == null) {
+ message =
+ deriveMessage(
+ type,
+ /* customMessage= */ null,
+ rendererName,
+ rendererIndex,
+ rendererFormat,
+ rendererFormatSupport);
+ }
+
+ @Nullable String causeClassName = bundle.getString(keyForField(FIELD_CAUSE_CLASS_NAME));
+ @Nullable String causeMessage = bundle.getString(keyForField(FIELD_CAUSE_MESSAGE));
+ @Nullable Throwable cause = null;
+ if (!TextUtils.isEmpty(causeClassName)) {
+ final Class> clazz;
+ try {
+ clazz =
+ Class.forName(
+ causeClassName,
+ /* initialize= */ true,
+ ExoPlaybackException.class.getClassLoader());
+ if (Throwable.class.isAssignableFrom(clazz)) {
+ cause = createThrowable(clazz, causeMessage);
+ }
+ } catch (Throwable e) {
+ // Intentionally catch Throwable to catch both Exception and Error.
+ cause = createRemoteException(causeMessage);
+ }
+ }
+
+ return new ExoPlaybackException(
+ message,
+ cause,
+ type,
+ rendererName,
+ rendererIndex,
+ rendererFormat,
+ rendererFormatSupport,
+ /* mediaPeriodId= */ null,
+ timestampMs,
+ isRecoverable);
+ }
+
+ // Creates a new {@link Throwable} with possibly @{code null} message.
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ private static Throwable createThrowable(Class> throwableClazz, @Nullable String message)
+ throws Exception {
+ return (Throwable) throwableClazz.getConstructor(String.class).newInstance(message);
+ }
+
+ // Creates a new {@link RemoteException} with possibly {@code null} message.
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ private static RemoteException createRemoteException(@Nullable String message) {
+ return new RemoteException(message);
+ }
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
index 0c003e6621..217b564820 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -18,9 +18,7 @@ package com.google.android.exoplayer2;
import android.os.Build;
import java.util.HashSet;
-/**
- * Information about the ExoPlayer library.
- */
+/** Information about the ExoPlayer library. */
public final class ExoPlayerLibraryInfo {
/**
@@ -30,11 +28,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
- public static final String VERSION = "2.13.3";
+ public static final String VERSION = "2.14.0";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.3";
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.14.0";
/**
* The version of the library expressed as an integer, for example 1002003.
@@ -44,7 +42,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final int VERSION_INT = 2013003;
+ public static final int VERSION_INT = 2014000;
/**
* The default user agent for requests made by the library.
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/HeartRating.java b/library/common/src/main/java/com/google/android/exoplayer2/HeartRating.java
new file mode 100644
index 0000000000..b656d0bd35
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/HeartRating.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 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;
+
+import static com.google.android.exoplayer2.util.Assertions.checkArgument;
+
+import android.os.Bundle;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import com.google.common.base.Objects;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A rating expressed as "heart" or "no heart". It can be used to indicate whether the content is a
+ * favorite.
+ */
+public final class HeartRating extends Rating {
+
+ private final boolean rated;
+ private final boolean isHeart;
+
+ /** Creates a unrated instance. */
+ public HeartRating() {
+ rated = false;
+ isHeart = false;
+ }
+
+ /**
+ * Creates a rated instance.
+ *
+ * @param isHeart {@code true} for "heart", {@code false} for "no heart".
+ */
+ public HeartRating(boolean isHeart) {
+ rated = true;
+ this.isHeart = isHeart;
+ }
+
+ @Override
+ public boolean isRated() {
+ return rated;
+ }
+
+ /** Returns whether the rating is "heart". */
+ public boolean isHeart() {
+ return isHeart;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(rated, isHeart);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof HeartRating)) {
+ return false;
+ }
+ HeartRating other = (HeartRating) obj;
+ return isHeart == other.isHeart && rated == other.rated;
+ }
+
+ // Bundleable implementation.
+
+ @RatingType private static final int TYPE = RATING_TYPE_HEART;
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FIELD_RATING_TYPE, FIELD_RATED, FIELD_IS_HEART})
+ private @interface FieldNumber {}
+
+ private static final int FIELD_RATED = 1;
+ private static final int FIELD_IS_HEART = 2;
+
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE);
+ bundle.putBoolean(keyForField(FIELD_RATED), rated);
+ bundle.putBoolean(keyForField(FIELD_IS_HEART), isHeart);
+ return bundle;
+ }
+
+ /** Object that can restore a {@link HeartRating} from a {@link Bundle}. */
+ public static final Creator CREATOR = HeartRating::fromBundle;
+
+ private static HeartRating fromBundle(Bundle bundle) {
+ checkArgument(
+ bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_DEFAULT)
+ == TYPE);
+ boolean isRated = bundle.getBoolean(keyForField(FIELD_RATED), /* defaultValue= */ false);
+ return isRated
+ ? new HeartRating(bundle.getBoolean(keyForField(FIELD_IS_HEART), /* defaultValue= */ false))
+ : new HeartRating();
+ }
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
+}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/IllegalSeekPositionException.java b/library/common/src/main/java/com/google/android/exoplayer2/IllegalSeekPositionException.java
index baa1cf3f79..745e86983f 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/IllegalSeekPositionException.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/IllegalSeekPositionException.java
@@ -16,8 +16,8 @@
package com.google.android.exoplayer2;
/**
- * Thrown when an attempt is made to seek to a position that does not exist in the player's
- * {@link Timeline}.
+ * Thrown when an attempt is made to seek to a position that does not exist in the player's {@link
+ * Timeline}.
*/
public final class IllegalSeekPositionException extends IllegalStateException {
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java
index 184c065e8a..8e9ecb27d1 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java
@@ -19,10 +19,15 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.net.Uri;
+import android.os.Bundle;
+import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
+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.Collections;
@@ -32,7 +37,7 @@ import java.util.Map;
import java.util.UUID;
/** Representation of a media item. */
-public final class MediaItem {
+public final class MediaItem implements Bundleable {
/**
* Creates a {@link MediaItem} for the given URI.
@@ -142,17 +147,17 @@ public final class MediaItem {
}
/**
- * Sets the optional media ID which identifies the media item. If not specified, {@link #setUri}
- * must be called and the string representation of {@link PlaybackProperties#uri} is used as the
- * media ID.
+ * Sets the optional media ID which identifies the media item.
+ *
+ *
By default {@link #DEFAULT_MEDIA_ID} is used.
*/
- public Builder setMediaId(@Nullable String mediaId) {
- this.mediaId = mediaId;
+ public Builder setMediaId(String mediaId) {
+ this.mediaId = checkNotNull(mediaId);
return this;
}
/**
- * Sets the optional URI. If not specified, {@link #setMediaId(String)} must be called.
+ * Sets the optional URI.
*
*
If {@code uri} is null or unset no {@link PlaybackProperties} object is created during
* {@link #build()} and any other {@code Builder} methods that would populate {@link
@@ -163,7 +168,7 @@ public final class MediaItem {
}
/**
- * Sets the optional URI. If not specified, {@link #setMediaId(String)} must be called.
+ * Sets the optional URI.
*
*
If {@code uri} is null or unset no {@link PlaybackProperties} object is created during
* {@link #build()} and any other {@code Builder} methods that would populate {@link
@@ -582,10 +587,9 @@ public final class MediaItem {
customCacheKey,
subtitles,
tag);
- mediaId = mediaId != null ? mediaId : uri.toString();
}
return new MediaItem(
- checkNotNull(mediaId),
+ mediaId != null ? mediaId : DEFAULT_MEDIA_ID,
new ClippingProperties(
clipStartPositionMs,
clipEndPositionMs,
@@ -599,7 +603,7 @@ public final class MediaItem {
liveMaxOffsetMs,
liveMinPlaybackSpeed,
liveMaxPlaybackSpeed),
- mediaMetadata != null ? mediaMetadata : new MediaMetadata.Builder().build());
+ mediaMetadata != null ? mediaMetadata : MediaMetadata.EMPTY);
}
}
@@ -836,7 +840,7 @@ public final class MediaItem {
}
/** Live playback configuration. */
- public static final class LiveConfiguration {
+ public static final class LiveConfiguration implements Bundleable {
/** A live playback configuration with unset values. */
public static final LiveConfiguration UNSET =
@@ -930,6 +934,53 @@ public final class MediaItem {
result = 31 * result + (maxPlaybackSpeed != 0 ? Float.floatToIntBits(maxPlaybackSpeed) : 0);
return result;
}
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_TARGET_OFFSET_MS,
+ FIELD_MIN_OFFSET_MS,
+ FIELD_MAX_OFFSET_MS,
+ FIELD_MIN_PLAYBACK_SPEED,
+ FIELD_MAX_PLAYBACK_SPEED
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_TARGET_OFFSET_MS = 0;
+ private static final int FIELD_MIN_OFFSET_MS = 1;
+ private static final int FIELD_MAX_OFFSET_MS = 2;
+ private static final int FIELD_MIN_PLAYBACK_SPEED = 3;
+ private static final int FIELD_MAX_PLAYBACK_SPEED = 4;
+
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putLong(keyForField(FIELD_TARGET_OFFSET_MS), targetOffsetMs);
+ bundle.putLong(keyForField(FIELD_MIN_OFFSET_MS), minOffsetMs);
+ bundle.putLong(keyForField(FIELD_MAX_OFFSET_MS), maxOffsetMs);
+ bundle.putFloat(keyForField(FIELD_MIN_PLAYBACK_SPEED), minPlaybackSpeed);
+ bundle.putFloat(keyForField(FIELD_MAX_PLAYBACK_SPEED), maxPlaybackSpeed);
+ return bundle;
+ }
+
+ /** Object that can restore {@link LiveConfiguration} from a {@link Bundle}. */
+ public static final Creator CREATOR =
+ bundle ->
+ new LiveConfiguration(
+ bundle.getLong(
+ keyForField(FIELD_TARGET_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET),
+ bundle.getLong(keyForField(FIELD_MIN_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET),
+ bundle.getLong(keyForField(FIELD_MAX_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET),
+ bundle.getFloat(
+ keyForField(FIELD_MIN_PLAYBACK_SPEED), /* defaultValue= */ C.RATE_UNSET),
+ bundle.getFloat(
+ keyForField(FIELD_MAX_PLAYBACK_SPEED), /* defaultValue= */ C.RATE_UNSET));
+
+ private static String keyForField(@LiveConfiguration.FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
}
/** Properties for a text track. */
@@ -1029,7 +1080,7 @@ public final class MediaItem {
}
/** Optionally clips the media item to a custom start and end position. */
- public static final class ClippingProperties {
+ public static final class ClippingProperties implements Bundleable {
/** The start position in milliseconds. This is a value larger than or equal to zero. */
public final long startPositionMs;
@@ -1095,8 +1146,59 @@ public final class MediaItem {
result = 31 * result + (startsAtKeyFrame ? 1 : 0);
return result;
}
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_START_POSITION_MS,
+ FIELD_END_POSITION_MS,
+ FIELD_RELATIVE_TO_LIVE_WINDOW,
+ FIELD_RELATIVE_TO_DEFAULT_POSITION,
+ FIELD_STARTS_AT_KEY_FRAME
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_START_POSITION_MS = 0;
+ private static final int FIELD_END_POSITION_MS = 1;
+ private static final int FIELD_RELATIVE_TO_LIVE_WINDOW = 2;
+ private static final int FIELD_RELATIVE_TO_DEFAULT_POSITION = 3;
+ private static final int FIELD_STARTS_AT_KEY_FRAME = 4;
+
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putLong(keyForField(FIELD_START_POSITION_MS), startPositionMs);
+ bundle.putLong(keyForField(FIELD_END_POSITION_MS), endPositionMs);
+ bundle.putBoolean(keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), relativeToLiveWindow);
+ bundle.putBoolean(keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), relativeToDefaultPosition);
+ bundle.putBoolean(keyForField(FIELD_STARTS_AT_KEY_FRAME), startsAtKeyFrame);
+ return bundle;
+ }
+
+ /** Object that can restore {@link ClippingProperties} from a {@link Bundle}. */
+ public static final Creator CREATOR =
+ bundle ->
+ new ClippingProperties(
+ bundle.getLong(keyForField(FIELD_START_POSITION_MS), /* defaultValue= */ 0),
+ bundle.getLong(
+ keyForField(FIELD_END_POSITION_MS), /* defaultValue= */ C.TIME_END_OF_SOURCE),
+ bundle.getBoolean(keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), false),
+ bundle.getBoolean(keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), false),
+ bundle.getBoolean(keyForField(FIELD_STARTS_AT_KEY_FRAME), false));
+
+ private static String keyForField(@ClippingProperties.FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
}
+ /**
+ * The default media ID that is used if the media ID is not explicitly set by {@link
+ * Builder#setMediaId(String)}.
+ */
+ public static final String DEFAULT_MEDIA_ID = "";
+
/** Identifies the media item. */
public final String mediaId;
@@ -1157,4 +1259,87 @@ public final class MediaItem {
result = 31 * result + mediaMetadata.hashCode();
return result;
}
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_MEDIA_ID,
+ FIELD_LIVE_CONFIGURATION,
+ FIELD_MEDIA_METADATA,
+ FIELD_CLIPPING_PROPERTIES
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_MEDIA_ID = 0;
+ private static final int FIELD_LIVE_CONFIGURATION = 1;
+ private static final int FIELD_MEDIA_METADATA = 2;
+ private static final int FIELD_CLIPPING_PROPERTIES = 3;
+
+ /**
+ * {@inheritDoc}
+ *
+ *
It omits the {@link #playbackProperties} field. The {@link #playbackProperties} of an
+ * instance restored by {@link #CREATOR} will always be {@code null}.
+ */
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString(keyForField(FIELD_MEDIA_ID), mediaId);
+ bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle());
+ bundle.putBundle(keyForField(FIELD_MEDIA_METADATA), mediaMetadata.toBundle());
+ bundle.putBundle(keyForField(FIELD_CLIPPING_PROPERTIES), clippingProperties.toBundle());
+ return bundle;
+ }
+
+ /**
+ * Object that can restore {@link MediaItem} from a {@link Bundle}.
+ *
+ *
This is the secondary title of the media, unrelated to closed captions.
+ */
+ public Builder setSubtitle(@Nullable CharSequence subtitle) {
+ this.subtitle = subtitle;
+ return this;
+ }
+
+ /** Sets the description. */
+ public Builder setDescription(@Nullable CharSequence description) {
+ this.description = description;
+ return this;
+ }
+
+ /** Sets the media {@link Uri}. */
+ public Builder setMediaUri(@Nullable Uri mediaUri) {
+ this.mediaUri = mediaUri;
+ return this;
+ }
+
+ /** Sets the user {@link Rating}. */
+ public Builder setUserRating(@Nullable Rating userRating) {
+ this.userRating = userRating;
+ return this;
+ }
+
+ /** Sets the overall {@link Rating}. */
+ public Builder setOverallRating(@Nullable Rating overallRating) {
+ this.overallRating = overallRating;
+ return this;
+ }
+
+ /**
+ * Sets all fields supported by the {@link Metadata.Entry entries} within the {@link Metadata}.
+ *
+ *
Fields are only set if the {@link Metadata.Entry} has an implementation for {@link
+ * Metadata.Entry#populateMediaMetadata(Builder)}.
+ *
+ *
In the event that multiple {@link Metadata.Entry} objects within the {@link Metadata}
+ * relate to the same {@link MediaMetadata} field, then the last one will be used.
+ */
+ public Builder populateFromMetadata(Metadata metadata) {
+ for (int i = 0; i < metadata.length(); i++) {
+ Metadata.Entry entry = metadata.get(i);
+ entry.populateMediaMetadata(this);
+ }
+ return this;
+ }
+
+ /**
+ * Sets all fields supported by the {@link Metadata.Entry entries} within the list of {@link
+ * Metadata}.
+ *
+ *
Fields are only set if the {@link Metadata.Entry} has an implementation for {@link
+ * Metadata.Entry#populateMediaMetadata(Builder)}.
+ *
+ *
In the event that multiple {@link Metadata.Entry} objects within any of the {@link
+ * Metadata} relate to the same {@link MediaMetadata} field, then the last one will be used.
+ */
+ public Builder populateFromMetadata(List metadataList) {
+ for (int i = 0; i < metadataList.size(); i++) {
+ Metadata metadata = metadataList.get(i);
+ for (int j = 0; j < metadata.length(); j++) {
+ Metadata.Entry entry = metadata.get(j);
+ entry.populateMediaMetadata(this);
+ }
+ }
+ return this;
+ }
+
/** Returns a new {@link MediaMetadata} instance with the current builder values. */
public MediaMetadata build() {
- return new MediaMetadata(title);
+ return new MediaMetadata(/* builder= */ this);
}
}
- /** Optional title. */
- @Nullable public final String title;
+ /** Empty {@link MediaMetadata}. */
+ public static final MediaMetadata EMPTY = new MediaMetadata.Builder().build();
- private MediaMetadata(@Nullable String title) {
- this.title = title;
+ /** Optional title. */
+ @Nullable public final CharSequence title;
+ /** Optional artist. */
+ @Nullable public final CharSequence artist;
+ /** Optional album title. */
+ @Nullable public final CharSequence albumTitle;
+ /** Optional album artist. */
+ @Nullable public final CharSequence albumArtist;
+ /** Optional display title. */
+ @Nullable public final CharSequence displayTitle;
+ /**
+ * Optional subtitle.
+ *
+ *
This is the secondary title of the media, unrelated to closed captions.
+ */
+ @Nullable public final CharSequence subtitle;
+ /** Optional description. */
+ @Nullable public final CharSequence description;
+ /** Optional media {@link Uri}. */
+ @Nullable public final Uri mediaUri;
+ /** Optional user {@link Rating}. */
+ @Nullable public final Rating userRating;
+ /** Optional overall {@link Rating}. */
+ @Nullable public final Rating overallRating;
+
+ private MediaMetadata(Builder builder) {
+ this.title = builder.title;
+ this.artist = builder.artist;
+ this.albumTitle = builder.albumTitle;
+ this.albumArtist = builder.albumArtist;
+ this.displayTitle = builder.displayTitle;
+ this.subtitle = builder.subtitle;
+ this.description = builder.description;
+ this.mediaUri = builder.mediaUri;
+ this.userRating = builder.userRating;
+ this.overallRating = builder.overallRating;
+ }
+
+ /** Returns a new {@link Builder} instance with the current {@link MediaMetadata} fields. */
+ public Builder buildUpon() {
+ return new Builder(/* mediaMetadata= */ this);
}
@Override
@@ -53,13 +224,117 @@ public final class MediaMetadata {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
- MediaMetadata other = (MediaMetadata) obj;
-
- return Util.areEqual(title, other.title);
+ MediaMetadata that = (MediaMetadata) obj;
+ return Util.areEqual(title, that.title)
+ && Util.areEqual(artist, that.artist)
+ && Util.areEqual(albumTitle, that.albumTitle)
+ && Util.areEqual(albumArtist, that.albumArtist)
+ && Util.areEqual(displayTitle, that.displayTitle)
+ && Util.areEqual(subtitle, that.subtitle)
+ && Util.areEqual(description, that.description)
+ && Util.areEqual(mediaUri, that.mediaUri)
+ && Util.areEqual(userRating, that.userRating)
+ && Util.areEqual(overallRating, that.overallRating);
}
@Override
public int hashCode() {
- return title == null ? 0 : title.hashCode();
+ return Objects.hashCode(
+ title,
+ artist,
+ albumTitle,
+ albumArtist,
+ displayTitle,
+ subtitle,
+ description,
+ mediaUri,
+ userRating,
+ overallRating);
+ }
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_TITLE,
+ FIELD_ARTIST,
+ FIELD_ALBUM_TITLE,
+ FIELD_ALBUM_ARTIST,
+ FIELD_DISPLAY_TITLE,
+ FIELD_SUBTITLE,
+ FIELD_DESCRIPTION,
+ FIELD_MEDIA_URI,
+ FIELD_USER_RATING,
+ FIELD_OVERALL_RATING,
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_TITLE = 0;
+ private static final int FIELD_ARTIST = 1;
+ private static final int FIELD_ALBUM_TITLE = 2;
+ private static final int FIELD_ALBUM_ARTIST = 3;
+ private static final int FIELD_DISPLAY_TITLE = 4;
+ private static final int FIELD_SUBTITLE = 5;
+ private static final int FIELD_DESCRIPTION = 6;
+ private static final int FIELD_MEDIA_URI = 7;
+ private static final int FIELD_USER_RATING = 8;
+ private static final int FIELD_OVERALL_RATING = 9;
+
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putCharSequence(keyForField(FIELD_TITLE), title);
+ bundle.putCharSequence(keyForField(FIELD_ARTIST), artist);
+ bundle.putCharSequence(keyForField(FIELD_ALBUM_TITLE), albumTitle);
+ bundle.putCharSequence(keyForField(FIELD_ALBUM_ARTIST), albumArtist);
+ bundle.putCharSequence(keyForField(FIELD_DISPLAY_TITLE), displayTitle);
+ bundle.putCharSequence(keyForField(FIELD_SUBTITLE), subtitle);
+ bundle.putCharSequence(keyForField(FIELD_DESCRIPTION), description);
+ bundle.putParcelable(keyForField(FIELD_MEDIA_URI), mediaUri);
+
+ if (userRating != null) {
+ bundle.putBundle(keyForField(FIELD_USER_RATING), userRating.toBundle());
+ }
+ if (overallRating != null) {
+ bundle.putBundle(keyForField(FIELD_OVERALL_RATING), overallRating.toBundle());
+ }
+
+ return bundle;
+ }
+
+ /** Object that can restore {@link MediaMetadata} from a {@link Bundle}. */
+ public static final Creator CREATOR = MediaMetadata::fromBundle;
+
+ private static MediaMetadata fromBundle(Bundle bundle) {
+ Builder builder = new Builder();
+ builder
+ .setTitle(bundle.getCharSequence(keyForField(FIELD_TITLE)))
+ .setArtist(bundle.getCharSequence(keyForField(FIELD_ARTIST)))
+ .setAlbumTitle(bundle.getCharSequence(keyForField(FIELD_ALBUM_TITLE)))
+ .setAlbumArtist(bundle.getCharSequence(keyForField(FIELD_ALBUM_ARTIST)))
+ .setDisplayTitle(bundle.getCharSequence(keyForField(FIELD_DISPLAY_TITLE)))
+ .setSubtitle(bundle.getCharSequence(keyForField(FIELD_SUBTITLE)))
+ .setDescription(bundle.getCharSequence(keyForField(FIELD_DESCRIPTION)))
+ .setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI)));
+
+ if (bundle.containsKey(keyForField(FIELD_USER_RATING))) {
+ @Nullable Bundle fieldBundle = bundle.getBundle(keyForField(FIELD_USER_RATING));
+ if (fieldBundle != null) {
+ builder.setUserRating(Rating.CREATOR.fromBundle(fieldBundle));
+ }
+ }
+ if (bundle.containsKey(keyForField(FIELD_OVERALL_RATING))) {
+ @Nullable Bundle fieldBundle = bundle.getBundle(keyForField(FIELD_OVERALL_RATING));
+ if (fieldBundle != null) {
+ builder.setUserRating(Rating.CREATOR.fromBundle(fieldBundle));
+ }
+ }
+
+ return builder.build();
+ }
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
}
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ParserException.java b/library/common/src/main/java/com/google/android/exoplayer2/ParserException.java
index e0cae0cf3a..716eceda94 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/ParserException.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/ParserException.java
@@ -17,9 +17,7 @@ package com.google.android.exoplayer2;
import java.io.IOException;
-/**
- * Thrown when an error occurs parsing media data and metadata.
- */
+/** Thrown when an error occurs parsing media data and metadata. */
public class ParserException extends IOException {
public ParserException() {
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/PercentageRating.java b/library/common/src/main/java/com/google/android/exoplayer2/PercentageRating.java
new file mode 100644
index 0000000000..1953e7e1d1
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/PercentageRating.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 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;
+
+import static com.google.android.exoplayer2.util.Assertions.checkArgument;
+
+import android.os.Bundle;
+import androidx.annotation.FloatRange;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import com.google.common.base.Objects;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** A rating expressed as a percentage. */
+public final class PercentageRating extends Rating {
+
+ private final float percent;
+
+ /** Creates a unrated instance. */
+ public PercentageRating() {
+ percent = RATING_UNSET;
+ }
+
+ /**
+ * Creates a rated instance with the given percentage.
+ *
+ * @param percent The percentage value of the rating.
+ */
+ public PercentageRating(@FloatRange(from = 0, to = 100) float percent) {
+ checkArgument(percent >= 0.0f && percent <= 100.0f, "percent must be in the range of [0, 100]");
+ this.percent = percent;
+ }
+
+ @Override
+ public boolean isRated() {
+ return percent != RATING_UNSET;
+ }
+
+ /**
+ * Returns the percent value of this rating. Will be within the range {@code [0f, 100f]}, or
+ * {@link #RATING_UNSET} if unrated.
+ */
+ public float getPercent() {
+ return percent;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(percent);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof PercentageRating)) {
+ return false;
+ }
+ return percent == ((PercentageRating) obj).percent;
+ }
+
+ // Bundleable implementation.
+
+ @RatingType private static final int TYPE = RATING_TYPE_PERCENTAGE;
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FIELD_RATING_TYPE, FIELD_PERCENT})
+ private @interface FieldNumber {}
+
+ private static final int FIELD_PERCENT = 1;
+
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE);
+ bundle.putFloat(keyForField(FIELD_PERCENT), percent);
+ return bundle;
+ }
+
+ /** Object that can restore a {@link PercentageRating} from a {@link Bundle}. */
+ public static final Creator CREATOR = PercentageRating::fromBundle;
+
+ private static PercentageRating fromBundle(Bundle bundle) {
+ checkArgument(
+ bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_DEFAULT)
+ == TYPE);
+ float percent = bundle.getFloat(keyForField(FIELD_PERCENT), /* defaultValue= */ RATING_UNSET);
+ return percent == RATING_UNSET ? new PercentageRating() : new PercentageRating(percent);
+ }
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
+}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/common/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
index ff4f262812..806bf11064 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
@@ -15,13 +15,18 @@
*/
package com.google.android.exoplayer2;
+import android.os.Bundle;
import androidx.annotation.CheckResult;
+import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/** Parameters that apply to playback, including speed setting. */
-public final class PlaybackParameters {
+public final class PlaybackParameters implements Bundleable {
/** The default playback parameters: real-time playback with no silence skipping. */
public static final PlaybackParameters DEFAULT = new PlaybackParameters(/* speed= */ 1f);
@@ -106,4 +111,34 @@ public final class PlaybackParameters {
public String toString() {
return Util.formatInvariant("PlaybackParameters(speed=%.2f, pitch=%.2f)", speed, pitch);
}
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FIELD_SPEED, FIELD_PITCH})
+ private @interface FieldNumber {}
+
+ private static final int FIELD_SPEED = 0;
+ private static final int FIELD_PITCH = 1;
+
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putFloat(keyForField(FIELD_SPEED), speed);
+ bundle.putFloat(keyForField(FIELD_PITCH), pitch);
+ return bundle;
+ }
+
+ /** Object that can restore {@link PlaybackParameters} from a {@link Bundle}. */
+ public static final Creator CREATOR =
+ bundle -> {
+ float speed = bundle.getFloat(keyForField(FIELD_SPEED), /* defaultValue= */ 1f);
+ float pitch = bundle.getFloat(keyForField(FIELD_PITCH), /* defaultValue= */ 1f);
+ return new PlaybackParameters(speed, pitch);
+ };
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java b/library/common/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java
similarity index 100%
rename from library/core/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java
rename to library/common/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Player.java b/library/common/src/main/java/com/google/android/exoplayer2/Player.java
index 3d58521357..50739cfc24 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/Player.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/Player.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2;
-import android.content.Context;
+import android.os.Bundle;
import android.os.Looper;
import android.view.Surface;
import android.view.SurfaceHolder;
@@ -25,7 +25,6 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioListener;
-import com.google.android.exoplayer2.audio.AuxEffectInfo;
import com.google.android.exoplayer2.device.DeviceInfo;
import com.google.android.exoplayer2.device.DeviceListener;
import com.google.android.exoplayer2.metadata.Metadata;
@@ -34,11 +33,11 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
-import com.google.android.exoplayer2.util.MutableFlags;
+import com.google.android.exoplayer2.util.ExoFlags;
import com.google.android.exoplayer2.util.Util;
-import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
import com.google.android.exoplayer2.video.VideoListener;
-import com.google.android.exoplayer2.video.spherical.CameraMotionListener;
+import com.google.android.exoplayer2.video.VideoSize;
+import com.google.common.base.Objects;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -48,6 +47,11 @@ import java.util.List;
* A media player interface defining traditional high-level functionality, such as the ability to
* play, pause, seek and query properties of the currently playing media.
*
+ *
This interface includes some convenience methods that can be implemented by calling other
+ * methods in the interface. {@link BasePlayer} implements these convenience methods so inheriting
+ * {@link BasePlayer} is recommended when implementing the interface so that only the minimal set of
+ * required methods can be implemented.
+ *
*
Some important properties of media players that implement this interface are:
*
*
@@ -55,326 +59,13 @@ import java.util.List;
* which can be obtained by calling {@link #getCurrentTimeline()}.
*
They can provide a {@link TrackGroupArray} defining the currently available tracks, which
* can be obtained by calling {@link #getCurrentTrackGroups()}.
- *
They contain a number of renderers, each of which is able to render tracks of a single type
- * (e.g. audio, video or text). The number of renderers and their respective track types can
- * be obtained by calling {@link #getRendererCount()} and {@link #getRendererType(int)}.
*
They can provide a {@link TrackSelectionArray} defining which of the currently available
- * tracks are selected to be rendered by each renderer. This can be obtained by calling {@link
+ * tracks are selected to be rendered. This can be obtained by calling {@link
* #getCurrentTrackSelections()}}.
*
*/
public interface Player {
- /** The audio component of a {@link Player}. */
- interface AudioComponent {
-
- /**
- * Adds a listener to receive audio events.
- *
- * @param listener The listener to register.
- */
- void addAudioListener(AudioListener listener);
-
- /**
- * Removes a listener of audio events.
- *
- * @param listener The listener to unregister.
- */
- void removeAudioListener(AudioListener listener);
-
- /**
- * Sets the attributes for audio playback, used by the underlying audio track. If not set, the
- * default audio attributes will be used. They are suitable for general media playback.
- *
- *
Setting the audio attributes during playback may introduce a short gap in audio output as
- * the audio track is recreated. A new audio session id will also be generated.
- *
- *
If tunneling is enabled by the track selector, the specified audio attributes will be
- * ignored, but they will take effect if audio is later played without tunneling.
- *
- *
If the device is running a build before platform API version 21, audio attributes cannot
- * be set directly on the underlying audio track. In this case, the usage will be mapped onto an
- * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.
- *
- *
If audio focus should be handled, the {@link AudioAttributes#usage} must be {@link
- * C#USAGE_MEDIA} or {@link C#USAGE_GAME}. Other usages will throw an {@link
- * IllegalArgumentException}.
- *
- * @param audioAttributes The attributes to use for audio playback.
- * @param handleAudioFocus True if the player should handle audio focus, false otherwise.
- */
- void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus);
-
- /** Returns the attributes for audio playback. */
- AudioAttributes getAudioAttributes();
-
- /**
- * Sets the ID of the audio session to attach to the underlying {@link
- * android.media.AudioTrack}.
- *
- *
The audio session ID can be generated using {@link C#generateAudioSessionIdV21(Context)}
- * for API 21+.
- *
- * @param audioSessionId The audio session ID, or {@link C#AUDIO_SESSION_ID_UNSET} if it should
- * be generated by the framework.
- */
- void setAudioSessionId(int audioSessionId);
-
- /** Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set. */
- int getAudioSessionId();
-
- /** Sets information on an auxiliary audio effect to attach to the underlying audio track. */
- void setAuxEffectInfo(AuxEffectInfo auxEffectInfo);
-
- /** Detaches any previously attached auxiliary audio effect from the underlying audio track. */
- void clearAuxEffectInfo();
-
- /**
- * Sets the audio volume, with 0 being silence and 1 being unity gain.
- *
- * @param audioVolume The audio volume.
- */
- void setVolume(float audioVolume);
-
- /** Returns the audio volume, with 0 being silence and 1 being unity gain. */
- float getVolume();
-
- /**
- * Sets whether skipping silences in the audio stream is enabled.
- *
- * @param skipSilenceEnabled Whether skipping silences in the audio stream is enabled.
- */
- void setSkipSilenceEnabled(boolean skipSilenceEnabled);
-
- /** Returns whether skipping silences in the audio stream is enabled. */
- boolean getSkipSilenceEnabled();
- }
-
- /** The video component of a {@link Player}. */
- interface VideoComponent {
-
- /**
- * Sets the {@link C.VideoScalingMode}.
- *
- * @param videoScalingMode The {@link C.VideoScalingMode}.
- */
- void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode);
-
- /** Returns the {@link C.VideoScalingMode}. */
- @C.VideoScalingMode
- int getVideoScalingMode();
-
- /**
- * Adds a listener to receive video events.
- *
- * @param listener The listener to register.
- */
- void addVideoListener(VideoListener listener);
-
- /**
- * Removes a listener of video events.
- *
- * @param listener The listener to unregister.
- */
- void removeVideoListener(VideoListener listener);
-
- /**
- * Sets a listener to receive video frame metadata events.
- *
- *
This method is intended to be called by the same component that sets the {@link Surface}
- * onto which video will be rendered. If using ExoPlayer's standard UI components, this method
- * should not be called directly from application code.
- *
- * @param listener The listener.
- */
- void setVideoFrameMetadataListener(VideoFrameMetadataListener listener);
-
- /**
- * Clears the listener which receives video frame metadata events if it matches the one passed.
- * Else does nothing.
- *
- * @param listener The listener to clear.
- */
- void clearVideoFrameMetadataListener(VideoFrameMetadataListener listener);
-
- /**
- * Sets a listener of camera motion events.
- *
- * @param listener The listener.
- */
- void setCameraMotionListener(CameraMotionListener listener);
-
- /**
- * Clears the listener which receives camera motion events if it matches the one passed. Else
- * does nothing.
- *
- * @param listener The listener to clear.
- */
- void clearCameraMotionListener(CameraMotionListener listener);
-
- /**
- * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView}
- * currently set on the player.
- */
- void clearVideoSurface();
-
- /**
- * Clears the {@link Surface} onto which video is being rendered if it matches the one passed.
- * Else does nothing.
- *
- * @param surface The surface to clear.
- */
- void clearVideoSurface(@Nullable Surface surface);
-
- /**
- * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for
- * tracking the lifecycle of the surface, and must clear the surface by calling {@code
- * setVideoSurface(null)} if the surface is destroyed.
- *
- *
If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link
- * SurfaceHolder} then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, {@link
- * #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} rather
- * than this method, since passing the holder allows the player to track the lifecycle of the
- * surface automatically.
- *
- * @param surface The {@link Surface}.
- */
- void setVideoSurface(@Nullable Surface surface);
-
- /**
- * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be
- * rendered. The player will track the lifecycle of the surface automatically.
- *
- * @param surfaceHolder The surface holder.
- */
- void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder);
-
- /**
- * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being
- * rendered if it matches the one passed. Else does nothing.
- *
- * @param surfaceHolder The surface holder to clear.
- */
- void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder);
-
- /**
- * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the
- * lifecycle of the surface automatically.
- *
- * @param surfaceView The surface view.
- */
- void setVideoSurfaceView(@Nullable SurfaceView surfaceView);
-
- /**
- * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one
- * passed. Else does nothing.
- *
- * @param surfaceView The texture view to clear.
- */
- void clearVideoSurfaceView(@Nullable SurfaceView surfaceView);
-
- /**
- * Sets the {@link TextureView} onto which video will be rendered. The player will track the
- * lifecycle of the surface automatically.
- *
- * @param textureView The texture view.
- */
- void setVideoTextureView(@Nullable TextureView textureView);
-
- /**
- * Clears the {@link TextureView} onto which video is being rendered if it matches the one
- * passed. Else does nothing.
- *
- * @param textureView The texture view to clear.
- */
- void clearVideoTextureView(@Nullable TextureView textureView);
- }
-
- /** The text component of a {@link Player}. */
- interface TextComponent {
-
- /**
- * Registers an output to receive text events.
- *
- * @param listener The output to register.
- */
- void addTextOutput(TextOutput listener);
-
- /**
- * Removes a text output.
- *
- * @param listener The output to remove.
- */
- void removeTextOutput(TextOutput listener);
-
- /** Returns the current {@link Cue Cues}. This list may be empty. */
- List getCurrentCues();
- }
-
- /** The metadata component of a {@link Player}. */
- interface MetadataComponent {
-
- /**
- * Adds a {@link MetadataOutput} to receive metadata.
- *
- * @param output The output to register.
- */
- void addMetadataOutput(MetadataOutput output);
-
- /**
- * Removes a {@link MetadataOutput}.
- *
- * @param output The output to remove.
- */
- void removeMetadataOutput(MetadataOutput output);
- }
-
- /** The device component of a {@link Player}. */
- interface DeviceComponent {
-
- /** Adds a listener to receive device events. */
- void addDeviceListener(DeviceListener listener);
-
- /** Removes a listener of device events. */
- void removeDeviceListener(DeviceListener listener);
-
- /** Gets the device information. */
- DeviceInfo getDeviceInfo();
-
- /**
- * Gets the current volume of the device.
- *
- *
For devices with {@link DeviceInfo#PLAYBACK_TYPE_LOCAL local playback}, the volume
- * returned by this method varies according to the current {@link C.StreamType stream type}. The
- * stream type is determined by {@link AudioAttributes#usage} which can be converted to stream
- * type with {@link Util#getStreamTypeForAudioUsage(int)}. The audio attributes can be set to
- * the player by calling {@link AudioComponent#setAudioAttributes}.
- *
- *
For devices with {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote playback}, the volume of
- * the remote device is returned.
- */
- int getDeviceVolume();
-
- /** Gets whether the device is muted or not. */
- boolean isDeviceMuted();
-
- /**
- * Sets the volume of the device.
- *
- * @param volume The volume to set.
- */
- void setDeviceVolume(int volume);
-
- /** Increases the volume of the device. */
- void increaseDeviceVolume();
-
- /** Decreases the volume of the device. */
- void decreaseDeviceVolume();
-
- /** Sets the mute state of the device. */
- void setDeviceMuted(boolean muted);
- }
-
/**
* Listener of changes in player state.
*
@@ -383,16 +74,18 @@ public interface Player {
*
Listeners can choose to implement individual events (e.g. {@link
* #onIsPlayingChanged(boolean)}) or {@link #onEvents(Player, Events)}, which is called after one
* or more events occurred together.
+ *
+ * @deprecated Use {@link Player.Listener}.
*/
+ @Deprecated
interface EventListener {
/**
* Called when the timeline has been refreshed.
*
- *
Note that if the timeline has changed then a position discontinuity may also have
- * occurred. For example, the current period index may have changed as a result of periods being
- * added or removed from the timeline. This will not be reported via a separate call to
- * {@link #onPositionDiscontinuity(int)}.
+ *
Note that the current window or period index may change as a result of a timeline change.
+ * If playback can't continue smoothly because of this timeline change, a separate {@link
+ * #onPositionDiscontinuity(PositionInfo, PositionInfo, int)} callback will be triggered.
*
*
{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration.
@@ -400,30 +93,9 @@ public interface Player {
* @param timeline The latest timeline. Never null, but may be empty.
* @param reason The {@link TimelineChangeReason} responsible for this timeline change.
*/
- @SuppressWarnings("deprecation")
- default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
- Object manifest = null;
- if (timeline.getWindowCount() == 1) {
- // Legacy behavior was to report the manifest for single window timelines only.
- Timeline.Window window = new Timeline.Window();
- manifest = timeline.getWindow(0, window).manifest;
- }
- // Call deprecated version.
- onTimelineChanged(timeline, manifest, reason);
- }
+ default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {}
/**
- * Called when the timeline and/or manifest has been refreshed.
- *
- *
Note that if the timeline has changed then a position discontinuity may also have
- * occurred. For example, the current period index may have changed as a result of periods being
- * added or removed from the timeline. This will not be reported via a separate call to
- * {@link #onPositionDiscontinuity(int)}.
- *
- * @param timeline The latest timeline. Never null, but may be empty.
- * @param manifest The latest manifest in case the timeline has a single window only. Always
- * null if the timeline has more than a single window.
- * @param reason The {@link TimelineChangeReason} responsible for this timeline change.
* @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be
* accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex,
* window).manifest} for a given window index.
@@ -455,8 +127,10 @@ public interface Player {
* other events that happen in the same {@link Looper} message queue iteration.
*
* @param trackGroups The available tracks. Never null, but may be of length zero.
- * @param trackSelections The track selections for each renderer. Never null and always of
- * length {@link #getRendererCount()}, but may contain null elements.
+ * @param trackSelections The selected tracks. Never null, but may contain null elements. A
+ * concrete implementation may include null elements if it has a fixed number of renderer
+ * components, wishes to report a TrackSelection for each of them, and has one or more
+ * renderer components that is not assigned any selected tracks.
*/
default void onTracksChanged(
TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}
@@ -480,6 +154,20 @@ public interface Player {
*/
default void onStaticMetadataChanged(List metadataList) {}
+ /**
+ * Called when the combined {@link MediaMetadata} changes.
+ *
+ *
The provided {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata}
+ * and the static and dynamic metadata sourced from {@link #onStaticMetadataChanged(List)} and
+ * {@link MetadataOutput#onMetadata(Metadata)}.
+ *
+ *
{@link #onEvents(Player, Events)} will also be called to report this event along with
+ * other events that happen in the same {@link Looper} message queue iteration.
+ *
+ * @param mediaMetadata The combined {@link MediaMetadata}.
+ */
+ default void onMediaMetadataChanged(MediaMetadata mediaMetadata) {}
+
/**
* Called when the player starts or stops loading the source.
*
@@ -488,15 +176,23 @@ public interface Player {
*
* @param isLoading Whether the source is currently being loaded.
*/
- @SuppressWarnings("deprecation")
- default void onIsLoadingChanged(boolean isLoading) {
- onLoadingChanged(isLoading);
- }
+ default void onIsLoadingChanged(boolean isLoading) {}
/** @deprecated Use {@link #onIsLoadingChanged(boolean)} instead. */
@Deprecated
default void onLoadingChanged(boolean isLoading) {}
+ /**
+ * Called when the value returned from {@link #isCommandAvailable(int)} changes for at least one
+ * {@link Command}.
+ *
+ *
{@link #onEvents(Player, Events)} will also be called to report this event along with
+ * other events that happen in the same {@link Looper} message queue iteration.
+ *
+ * @param availableCommands The available {@link Commands}.
+ */
+ default void onAvailableCommandsChanged(Commands availableCommands) {}
+
/**
* @deprecated Use {@link #onPlaybackStateChanged(int)} and {@link
* #onPlayWhenReadyChanged(boolean, int)} instead.
@@ -580,21 +276,27 @@ public interface Player {
default void onPlayerError(ExoPlaybackException error) {}
/**
- * Called when a position discontinuity occurs without a change to the timeline. A position
- * discontinuity occurs when the current window or period index changes (as a result of playback
- * transitioning from one period in the timeline to the next), or when the playback position
- * jumps within the period currently being played (as a result of a seek being performed, or
- * when the source introduces a discontinuity internally).
+ * @deprecated Use {@link #onPositionDiscontinuity(PositionInfo, PositionInfo, int)} instead.
+ */
+ @Deprecated
+ default void onPositionDiscontinuity(@DiscontinuityReason int reason) {}
+
+ /**
+ * Called when a position discontinuity occurs.
*
- *
When a position discontinuity occurs as a result of a change to the timeline this method
- * is not called. {@link #onTimelineChanged(Timeline, int)} is called in this case.
+ *
A position discontinuity occurs when the playing period changes, the playback position
+ * jumps within the period currently being played, or when the playing period has been skipped
+ * or removed.
*
*
{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration.
*
+ * @param oldPosition The position before the discontinuity.
+ * @param newPosition The position after the discontinuity.
* @param reason The {@link DiscontinuityReason} responsible for the discontinuity.
*/
- default void onPositionDiscontinuity(@DiscontinuityReason int reason) {}
+ default void onPositionDiscontinuity(
+ PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) {}
/**
* Called when the current playback parameters change. The playback parameters may change due to
@@ -611,29 +313,12 @@ public interface Player {
/**
* @deprecated Seeks are processed without delay. Listen to {@link
- * #onPositionDiscontinuity(int)} with reason {@link #DISCONTINUITY_REASON_SEEK} instead.
+ * #onPositionDiscontinuity(PositionInfo, PositionInfo, int)} with reason {@link
+ * #DISCONTINUITY_REASON_SEEK} instead.
*/
@Deprecated
default void onSeekProcessed() {}
- /**
- * Called when the player has started or stopped offload scheduling.
- *
- *
If using ExoPlayer, this is done by calling {@code
- * ExoPlayer#experimentalSetOffloadSchedulingEnabled(boolean)}.
- *
- *
This method is experimental, and will be renamed or removed in a future release.
- */
- // TODO(b/172315872) Move this method in a new ExoPlayer.EventListener.
- default void onExperimentalOffloadSchedulingEnabledChanged(boolean offloadSchedulingEnabled) {}
-
- /**
- * Called when the player has started or finished sleeping for offload.
- *
- *
This method is experimental, and will be renamed or removed in a future release.
- */
- default void onExperimentalSleepingForOffloadChanged(boolean sleepingForOffload) {}
-
/**
* Called when one or more player states changed.
*
@@ -665,44 +350,28 @@ public interface Player {
default void onEvents(Player player, Events events) {}
}
- /**
- * @deprecated Use {@link EventListener} interface directly for selective overrides as all methods
- * are implemented as no-op default methods.
- */
- @Deprecated
- abstract class DefaultEventListener implements EventListener {
-
- @Override
- public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
- Object manifest = null;
- if (timeline.getWindowCount() == 1) {
- // Legacy behavior was to report the manifest for single window timelines only.
- Timeline.Window window = new Timeline.Window();
- manifest = timeline.getWindow(0, window).manifest;
- }
- // Call deprecated version.
- onTimelineChanged(timeline, manifest, reason);
- }
-
- @Override
- public void onTimelineChanged(
- Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
- // Do nothing.
- }
- }
-
/** A set of {@link EventFlags}. */
- final class Events extends MutableFlags {
+ final class Events {
+
+ private final ExoFlags flags;
+
+ /**
+ * Creates an instance.
+ *
+ * @param flags The {@link ExoFlags} containing the {@link EventFlags} in the set.
+ */
+ public Events(ExoFlags flags) {
+ this.flags = flags;
+ }
+
/**
* Returns whether the given event occurred.
*
* @param event The {@link EventFlags event}.
* @return Whether the event occurred.
*/
- @Override
public boolean contains(@EventFlags int event) {
- // Overridden to add IntDef compiler enforcement and new JavaDoc.
- return super.contains(event);
+ return flags.contains(event);
}
/**
@@ -711,10 +380,13 @@ public interface Player {
* @param events The {@link EventFlags events}.
* @return Whether any of the events occurred.
*/
- @Override
public boolean containsAny(@EventFlags int... events) {
- // Overridden to add IntDef compiler enforcement and new JavaDoc.
- return super.containsAny(events);
+ return flags.containsAny(events);
+ }
+
+ /** Returns the number of events in the set. */
+ public int size() {
+ return flags.size();
}
/**
@@ -725,15 +397,317 @@ public interface Player {
*
* @param index The index. Must be between 0 (inclusive) and {@link #size()} (exclusive).
* @return The {@link EventFlags event} at the given index.
+ * @throws IndexOutOfBoundsException If index is outside the allowed range.
*/
- @Override
@EventFlags
public int get(int index) {
- // Overridden to add IntDef compiler enforcement and new JavaDoc.
- return super.get(index);
+ return flags.get(index);
}
}
+ /** Position info describing a playback position involved in a discontinuity. */
+ final class PositionInfo implements Bundleable {
+
+ /**
+ * The UID of the window, or {@code null}, if the timeline is {@link Timeline#isEmpty() empty}.
+ */
+ @Nullable public final Object windowUid;
+ /** The window index. */
+ public final int windowIndex;
+ /**
+ * The UID of the period, or {@code null}, if the timeline is {@link Timeline#isEmpty() empty}.
+ */
+ @Nullable public final Object periodUid;
+ /** The period index. */
+ public final int periodIndex;
+ /** The playback position, in milliseconds. */
+ public final long positionMs;
+ /**
+ * The content position, in milliseconds.
+ *
+ *
If {@link #adGroupIndex} is {@link C#INDEX_UNSET}, this is the same as {@link
+ * #positionMs}.
+ */
+ public final long contentPositionMs;
+ /**
+ * The ad group index if the playback position is within an ad, {@link C#INDEX_UNSET} otherwise.
+ */
+ public final int adGroupIndex;
+ /**
+ * The index of the ad within the ad group if the playback position is within an ad, {@link
+ * C#INDEX_UNSET} otherwise.
+ */
+ public final int adIndexInAdGroup;
+
+ /** Creates an instance. */
+ public PositionInfo(
+ @Nullable Object windowUid,
+ int windowIndex,
+ @Nullable Object periodUid,
+ int periodIndex,
+ long positionMs,
+ long contentPositionMs,
+ int adGroupIndex,
+ int adIndexInAdGroup) {
+ this.windowUid = windowUid;
+ this.windowIndex = windowIndex;
+ this.periodUid = periodUid;
+ this.periodIndex = periodIndex;
+ this.positionMs = positionMs;
+ this.contentPositionMs = contentPositionMs;
+ 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;
+ }
+ PositionInfo that = (PositionInfo) o;
+ return windowIndex == that.windowIndex
+ && periodIndex == that.periodIndex
+ && positionMs == that.positionMs
+ && contentPositionMs == that.contentPositionMs
+ && adGroupIndex == that.adGroupIndex
+ && adIndexInAdGroup == that.adIndexInAdGroup
+ && Objects.equal(windowUid, that.windowUid)
+ && Objects.equal(periodUid, that.periodUid);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(
+ windowUid,
+ windowIndex,
+ periodUid,
+ periodIndex,
+ windowIndex,
+ positionMs,
+ contentPositionMs,
+ adGroupIndex,
+ adIndexInAdGroup);
+ }
+
+ // Bundleable implementation.
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_WINDOW_INDEX,
+ FIELD_PERIOD_INDEX,
+ FIELD_POSITION_MS,
+ FIELD_CONTENT_POSITION_MS,
+ FIELD_AD_GROUP_INDEX,
+ FIELD_AD_INDEX_IN_AD_GROUP
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_WINDOW_INDEX = 0;
+ private static final int FIELD_PERIOD_INDEX = 1;
+ private static final int FIELD_POSITION_MS = 2;
+ private static final int FIELD_CONTENT_POSITION_MS = 3;
+ private static final int FIELD_AD_GROUP_INDEX = 4;
+ private static final int FIELD_AD_INDEX_IN_AD_GROUP = 5;
+
+ /**
+ * {@inheritDoc}
+ *
+ *
It omits the {@link #windowUid} and {@link #periodUid} fields. The {@link #windowUid} and
+ * {@link #periodUid} of an instance restored by {@link #CREATOR} will always be {@code null}.
+ */
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(keyForField(FIELD_WINDOW_INDEX), windowIndex);
+ bundle.putInt(keyForField(FIELD_PERIOD_INDEX), periodIndex);
+ bundle.putLong(keyForField(FIELD_POSITION_MS), positionMs);
+ bundle.putLong(keyForField(FIELD_CONTENT_POSITION_MS), contentPositionMs);
+ bundle.putInt(keyForField(FIELD_AD_GROUP_INDEX), adGroupIndex);
+ bundle.putInt(keyForField(FIELD_AD_INDEX_IN_AD_GROUP), adIndexInAdGroup);
+ return bundle;
+ }
+
+ /** Object that can restore {@link PositionInfo} from a {@link Bundle}. */
+ public static final Creator CREATOR = PositionInfo::fromBundle;
+
+ private static PositionInfo fromBundle(Bundle bundle) {
+ int windowIndex =
+ bundle.getInt(keyForField(FIELD_WINDOW_INDEX), /* defaultValue= */ C.INDEX_UNSET);
+ int periodIndex =
+ bundle.getInt(keyForField(FIELD_PERIOD_INDEX), /* defaultValue= */ C.INDEX_UNSET);
+ long positionMs =
+ bundle.getLong(keyForField(FIELD_POSITION_MS), /* defaultValue= */ C.TIME_UNSET);
+ long contentPositionMs =
+ bundle.getLong(keyForField(FIELD_CONTENT_POSITION_MS), /* defaultValue= */ C.TIME_UNSET);
+ int adGroupIndex =
+ bundle.getInt(keyForField(FIELD_AD_GROUP_INDEX), /* defaultValue= */ C.INDEX_UNSET);
+ int adIndexInAdGroup =
+ bundle.getInt(keyForField(FIELD_AD_INDEX_IN_AD_GROUP), /* defaultValue= */ C.INDEX_UNSET);
+ return new PositionInfo(
+ /* windowUid= */ null,
+ windowIndex,
+ /* periodUid= */ null,
+ periodIndex,
+ positionMs,
+ contentPositionMs,
+ adGroupIndex,
+ adIndexInAdGroup);
+ }
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
+ }
+
+ /**
+ * A set of {@link Command commands}.
+ *
+ *
Instances are immutable.
+ */
+ final class Commands {
+
+ /** A builder for {@link Commands} instances. */
+ public static final class Builder {
+
+ private final ExoFlags.Builder flagsBuilder;
+
+ /** Creates a builder. */
+ public Builder() {
+ flagsBuilder = new ExoFlags.Builder();
+ }
+
+ /**
+ * Adds a {@link Command}.
+ *
+ * @param command A {@link Command}.
+ * @return This builder.
+ * @throws IllegalStateException If {@link #build()} has already been called.
+ */
+ public Builder add(@Command int command) {
+ flagsBuilder.add(command);
+ return this;
+ }
+
+ /**
+ * Adds a {@link Command} if the provided condition is true. Does nothing otherwise.
+ *
+ * @param command A {@link Command}.
+ * @param condition A condition.
+ * @return This builder.
+ * @throws IllegalStateException If {@link #build()} has already been called.
+ */
+ public Builder addIf(@Command int command, boolean condition) {
+ flagsBuilder.addIf(command, condition);
+ return this;
+ }
+
+ /**
+ * Adds {@link Command commands}.
+ *
+ * @param commands The {@link Command commands} to add.
+ * @return This builder.
+ * @throws IllegalStateException If {@link #build()} has already been called.
+ */
+ public Builder addAll(@Command int... commands) {
+ flagsBuilder.addAll(commands);
+ return this;
+ }
+
+ /**
+ * Adds {@link Commands}.
+ *
+ * @param commands The set of {@link Command commands} to add.
+ * @return This builder.
+ * @throws IllegalStateException If {@link #build()} has already been called.
+ */
+ public Builder addAll(Commands commands) {
+ flagsBuilder.addAll(commands.flags);
+ return this;
+ }
+
+ /**
+ * Builds a {@link Commands} instance.
+ *
+ * @throws IllegalStateException If this method has already been called.
+ */
+ public Commands build() {
+ return new Commands(flagsBuilder.build());
+ }
+ }
+
+ /** An empty set of commands. */
+ public static final Commands EMPTY = new Builder().build();
+
+ private final ExoFlags flags;
+
+ private Commands(ExoFlags flags) {
+ this.flags = flags;
+ }
+
+ /** Returns whether the set of commands contains the specified {@link Command}. */
+ public boolean contains(@Command int command) {
+ return flags.contains(command);
+ }
+
+ /** Returns the number of commands in this set. */
+ public int size() {
+ return flags.size();
+ }
+
+ /**
+ * Returns the {@link Command} at the given index.
+ *
+ * @param index The index. Must be between 0 (inclusive) and {@link #size()} (exclusive).
+ * @return The {@link Command} at the given index.
+ * @throws IndexOutOfBoundsException If index is outside the allowed range.
+ */
+ @Command
+ public int get(int index) {
+ return flags.get(index);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Commands)) {
+ return false;
+ }
+ Commands commands = (Commands) obj;
+ return flags.equals(commands.flags);
+ }
+
+ @Override
+ public int hashCode() {
+ return flags.hashCode();
+ }
+ }
+
+ /**
+ * Listener of all changes in the Player.
+ *
+ *
All methods have no-op default implementations to allow selective overrides.
+ */
+ interface Listener
+ extends VideoListener,
+ AudioListener,
+ TextOutput,
+ MetadataOutput,
+ DeviceListener,
+ EventListener {
+
+ // For backward compatibility TextOutput and MetadataOutput must stay functional interfaces.
+ @Override
+ default void onCues(List cues) {}
+
+ @Override
+ default void onMetadata(Metadata metadata) {}
+ }
+
/**
* Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or
* {@link #STATE_ENDED}.
@@ -831,36 +805,44 @@ public interface Player {
int REPEAT_MODE_ALL = 2;
/**
- * Reasons for position discontinuities. One of {@link #DISCONTINUITY_REASON_PERIOD_TRANSITION},
+ * Reasons for position discontinuities. One of {@link #DISCONTINUITY_REASON_AUTO_TRANSITION},
* {@link #DISCONTINUITY_REASON_SEEK}, {@link #DISCONTINUITY_REASON_SEEK_ADJUSTMENT}, {@link
- * #DISCONTINUITY_REASON_AD_INSERTION} or {@link #DISCONTINUITY_REASON_INTERNAL}.
+ * #DISCONTINUITY_REASON_SKIP}, {@link #DISCONTINUITY_REASON_REMOVE} or {@link
+ * #DISCONTINUITY_REASON_INTERNAL}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
- DISCONTINUITY_REASON_PERIOD_TRANSITION,
+ DISCONTINUITY_REASON_AUTO_TRANSITION,
DISCONTINUITY_REASON_SEEK,
DISCONTINUITY_REASON_SEEK_ADJUSTMENT,
- DISCONTINUITY_REASON_AD_INSERTION,
+ DISCONTINUITY_REASON_SKIP,
+ DISCONTINUITY_REASON_REMOVE,
DISCONTINUITY_REASON_INTERNAL
})
@interface DiscontinuityReason {}
/**
- * Automatic playback transition from one period in the timeline to the next. The period index may
- * be the same as it was before the discontinuity in case the current period is repeated.
+ * Automatic playback transition from one period in the timeline to the next without explicit
+ * interaction by this player. The period index may be the same as it was before the discontinuity
+ * in case the current period is repeated.
+ *
+ *
This reason also indicates an automatic transition from the content period to an inserted ad
+ * period or vice versa.
*/
- int DISCONTINUITY_REASON_PERIOD_TRANSITION = 0;
- /** Seek within the current period or to another period. */
+ int DISCONTINUITY_REASON_AUTO_TRANSITION = 0;
+ /** Seek within the current period or to another period by this player. */
int DISCONTINUITY_REASON_SEEK = 1;
/**
* Seek adjustment due to being unable to seek to the requested position or because the seek was
* permitted to be inexact.
*/
int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2;
- /** Discontinuity to or from an ad within one period in the timeline. */
- int DISCONTINUITY_REASON_AD_INSERTION = 3;
- /** Discontinuity introduced internally by the source. */
- int DISCONTINUITY_REASON_INTERNAL = 4;
+ /** Discontinuity introduced by a skipped period (for instance a skipped ad). */
+ int DISCONTINUITY_REASON_SKIP = 3;
+ /** Discontinuity caused by the removal of the current period from the {@link Timeline}. */
+ int DISCONTINUITY_REASON_REMOVE = 4;
+ /** Discontinuity introduced internally (e.g. by the source). */
+ int DISCONTINUITY_REASON_INTERNAL = 5;
/**
* Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED} or {@link
@@ -903,7 +885,7 @@ public interface Player {
int MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED = 3;
/**
- * Events that can be reported via {@link EventListener#onEvents(Player, Events)}.
+ * Events that can be reported via {@link Listener#onEvents(Player, Events)}.
*
*
One of the {@link Player}{@code .EVENT_*} flags.
*/
@@ -923,7 +905,9 @@ public interface Player {
EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
EVENT_PLAYER_ERROR,
EVENT_POSITION_DISCONTINUITY,
- EVENT_PLAYBACK_PARAMETERS_CHANGED
+ EVENT_PLAYBACK_PARAMETERS_CHANGED,
+ EVENT_AVAILABLE_COMMANDS_CHANGED,
+ EVENT_MEDIA_METADATA_CHANGED
})
@interface EventFlags {}
/** {@link #getCurrentTimeline()} changed. */
@@ -950,32 +934,102 @@ public interface Player {
int EVENT_SHUFFLE_MODE_ENABLED_CHANGED = 10;
/** {@link #getPlayerError()} changed. */
int EVENT_PLAYER_ERROR = 11;
- /** A position discontinuity occurred. See {@link EventListener#onPositionDiscontinuity(int)}. */
+ /**
+ * A position discontinuity occurred. See {@link Listener#onPositionDiscontinuity(PositionInfo,
+ * PositionInfo, int)}.
+ */
int EVENT_POSITION_DISCONTINUITY = 12;
/** {@link #getPlaybackParameters()} changed. */
int EVENT_PLAYBACK_PARAMETERS_CHANGED = 13;
-
- /** Returns the component of this player for audio output, or null if audio is not supported. */
- @Nullable
- AudioComponent getAudioComponent();
-
- /** Returns the component of this player for video output, or null if video is not supported. */
- @Nullable
- VideoComponent getVideoComponent();
-
- /** Returns the component of this player for text output, or null if text is not supported. */
- @Nullable
- TextComponent getTextComponent();
+ /** {@link #isCommandAvailable(int)} changed for at least one {@link Command}. */
+ int EVENT_AVAILABLE_COMMANDS_CHANGED = 14;
+ /** {@link #getMediaMetadata()} changed. */
+ int EVENT_MEDIA_METADATA_CHANGED = 15;
/**
- * Returns the component of this player for metadata output, or null if metadata is not supported.
+ * Commands that can be executed on a {@code Player}. One of {@link #COMMAND_PLAY_PAUSE}, {@link
+ * #COMMAND_PREPARE_STOP}, {@link #COMMAND_SEEK_TO_DEFAULT_POSITION}, {@link
+ * #COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM}, {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM}, {@link
+ * #COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM}, {@link #COMMAND_SEEK_TO_MEDIA_ITEM}, {@link
+ * #COMMAND_SET_SPEED_AND_PITCH}, {@link #COMMAND_SET_SHUFFLE_MODE}, {@link
+ * #COMMAND_SET_REPEAT_MODE}, {@link #COMMAND_GET_CURRENT_MEDIA_ITEM}, {@link
+ * #COMMAND_GET_MEDIA_ITEMS}, {@link #COMMAND_GET_MEDIA_ITEMS_METADATA}, {@link
+ * #COMMAND_CHANGE_MEDIA_ITEMS}, {@link #COMMAND_GET_AUDIO_ATTRIBUTES}, {@link
+ * #COMMAND_GET_VOLUME}, {@link #COMMAND_GET_DEVICE_VOLUME}, {@link #COMMAND_SET_VOLUME}, {@link
+ * #COMMAND_SET_DEVICE_VOLUME}, {@link #COMMAND_ADJUST_DEVICE_VOLUME}, {@link
+ * #COMMAND_SET_VIDEO_SURFACE} or {@link #COMMAND_GET_TEXT}.
*/
- @Nullable
- MetadataComponent getMetadataComponent();
-
- /** Returns the component of this player for playback device, or null if it's not supported. */
- @Nullable
- DeviceComponent getDeviceComponent();
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ COMMAND_PLAY_PAUSE,
+ COMMAND_PREPARE_STOP,
+ COMMAND_SEEK_TO_DEFAULT_POSITION,
+ COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
+ COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
+ COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
+ COMMAND_SEEK_TO_MEDIA_ITEM,
+ COMMAND_SET_SPEED_AND_PITCH,
+ COMMAND_SET_SHUFFLE_MODE,
+ COMMAND_SET_REPEAT_MODE,
+ COMMAND_GET_CURRENT_MEDIA_ITEM,
+ COMMAND_GET_MEDIA_ITEMS,
+ COMMAND_GET_MEDIA_ITEMS_METADATA,
+ COMMAND_CHANGE_MEDIA_ITEMS,
+ COMMAND_GET_AUDIO_ATTRIBUTES,
+ COMMAND_GET_VOLUME,
+ COMMAND_GET_DEVICE_VOLUME,
+ COMMAND_SET_VOLUME,
+ COMMAND_SET_DEVICE_VOLUME,
+ COMMAND_ADJUST_DEVICE_VOLUME,
+ COMMAND_SET_VIDEO_SURFACE,
+ COMMAND_GET_TEXT
+ })
+ @interface Command {}
+ /** Command to start, pause or resume playback. */
+ int COMMAND_PLAY_PAUSE = 1;
+ /** Command to prepare the player, stop playback or release the player. */
+ int COMMAND_PREPARE_STOP = 2;
+ /** Command to seek to the default position of the current window. */
+ int COMMAND_SEEK_TO_DEFAULT_POSITION = 3;
+ /** Command to seek anywhere into the current window. */
+ int COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM = 4;
+ /** Command to seek to the default position of the next window. */
+ int COMMAND_SEEK_TO_NEXT_MEDIA_ITEM = 5;
+ /** Command to seek to the default position of the previous window. */
+ int COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM = 6;
+ /** Command to seek anywhere in any window. */
+ int COMMAND_SEEK_TO_MEDIA_ITEM = 7;
+ /** Command to set the playback speed and pitch. */
+ int COMMAND_SET_SPEED_AND_PITCH = 8;
+ /** Command to enable shuffling. */
+ int COMMAND_SET_SHUFFLE_MODE = 9;
+ /** Command to set the repeat mode. */
+ int COMMAND_SET_REPEAT_MODE = 10;
+ /** Command to get the {@link MediaItem} of the current window. */
+ int COMMAND_GET_CURRENT_MEDIA_ITEM = 11;
+ /** Command to get the current timeline and its {@link MediaItem MediaItems}. */
+ int COMMAND_GET_MEDIA_ITEMS = 12;
+ /** Command to get the {@link MediaItem MediaItems} metadata. */
+ int COMMAND_GET_MEDIA_ITEMS_METADATA = 13;
+ /** Command to change the {@link MediaItem MediaItems} in the playlist. */
+ int COMMAND_CHANGE_MEDIA_ITEMS = 14;
+ /** Command to get the player current {@link AudioAttributes}. */
+ int COMMAND_GET_AUDIO_ATTRIBUTES = 15;
+ /** Command to get the player volume. */
+ int COMMAND_GET_VOLUME = 16;
+ /** Command to get the device volume and whether it is muted. */
+ int COMMAND_GET_DEVICE_VOLUME = 17;
+ /** Command to set the player volume. */
+ int COMMAND_SET_VOLUME = 18;
+ /** Command to set the device volume and mute it. */
+ int COMMAND_SET_DEVICE_VOLUME = 19;
+ /** Command to increase and decrease the device volume and mute it. */
+ int COMMAND_ADJUST_DEVICE_VOLUME = 20;
+ /** Command to set and clear the surface on which to render the video. */
+ int COMMAND_SET_VIDEO_SURFACE = 21;
+ /** Command to get the text that should currently be displayed by the player. */
+ int COMMAND_GET_TEXT = 22;
/**
* Returns the {@link Looper} associated with the application thread that's used to access the
@@ -984,20 +1038,40 @@ public interface Player {
Looper getApplicationLooper();
/**
- * Register a listener to receive events from the player. The listener's methods will be called on
- * the thread that was used to construct the player. However, if the thread used to construct the
- * player does not have a {@link Looper}, then the listener will be called on the main thread.
+ * Registers a listener to receive events from the player. The listener's methods will be called
+ * on the thread that was used to construct the player. However, if the thread used to construct
+ * the player does not have a {@link Looper}, then the listener will be called on the main thread.
*
* @param listener The listener to register.
+ * @deprecated Use {@link #addListener(Listener)} and {@link #removeListener(Listener)} instead.
*/
+ @Deprecated
void addListener(EventListener listener);
/**
- * Unregister a listener. The listener will no longer receive events from the player.
+ * Registers a listener to receive all events from the player.
+ *
+ * @param listener The listener to register.
+ */
+ void addListener(Listener listener);
+
+ /**
+ * Unregister a listener registered through {@link #addListener(EventListener)}. The listener will
+ * no longer receive events from the player.
+ *
+ * @param listener The listener to unregister.
+ * @deprecated Use {@link #addListener(Listener)} and {@link #removeListener(Listener)} instead.
+ */
+ @Deprecated
+ void removeListener(EventListener listener);
+
+ /**
+ * Unregister a listener registered through {@link #addListener(Listener)}. The listener will no
+ * longer receive events.
*
* @param listener The listener to unregister.
*/
- void removeListener(EventListener listener);
+ void removeListener(Listener listener);
/**
* Clears the playlist, adds the specified {@link MediaItem MediaItems} and resets the position to
@@ -1027,7 +1101,7 @@ public interface Player {
* C#TIME_UNSET} is passed, the default position of the given window is used. In any case, if
* {@code startWindowIndex} is set to {@link C#INDEX_UNSET}, this parameter is ignored and the
* position is not reset at all.
- * @throws IllegalSeekPositionException If the provided {@code windowIndex} is not within the
+ * @throws IllegalSeekPositionException If the provided {@code startWindowIndex} is not within the
* bounds of the list of media items.
*/
void setMediaItems(List mediaItems, int startWindowIndex, long startPositionMs);
@@ -1068,7 +1142,8 @@ public interface Player {
/**
* Adds a media item at the given index of the playlist.
*
- * @param index The index at which to add the item.
+ * @param index The index at which to add the media item. If the index is larger than the size of
+ * the playlist, the media item is added to the end of the playlist.
* @param mediaItem The {@link MediaItem} to add.
*/
void addMediaItem(int index, MediaItem mediaItem);
@@ -1083,7 +1158,8 @@ public interface Player {
/**
* Adds a list of media items at the given index of the playlist.
*
- * @param index The index at which to add the media items.
+ * @param index The index at which to add the media items. If the index is larger than the size of
+ * the playlist, the media items are added to the end of the playlist.
* @param mediaItems The {@link MediaItem MediaItems} to add.
*/
void addMediaItems(int index, List mediaItems);
@@ -1119,13 +1195,51 @@ public interface Player {
* Removes a range of media items from the playlist.
*
* @param fromIndex The index at which to start removing media items.
- * @param toIndex The index of the first item to be kept (exclusive).
+ * @param toIndex The index of the first item to be kept (exclusive). If the index is larger than
+ * the size of the playlist, media items to the end of the playlist are removed.
*/
void removeMediaItems(int fromIndex, int toIndex);
/** Clears the playlist. */
void clearMediaItems();
+ /**
+ * Returns whether the provided {@link Command} is available.
+ *
+ *
This method does not execute the command.
+ *
+ *
Executing a command that is not available (for example, calling {@link #next()} if {@link
+ * #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} is unavailable) will neither throw an exception nor generate
+ * a {@link #getPlayerError()} player error}.
+ *
+ *
{@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} and {@link #COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM}
+ * are unavailable if there is no such {@link MediaItem}.
+ *
+ * @param command A {@link Command}.
+ * @return Whether the {@link Command} is available.
+ * @see Listener#onAvailableCommandsChanged(Commands)
+ */
+ boolean isCommandAvailable(@Command int command);
+
+ /**
+ * Returns the player's currently available {@link Commands}.
+ *
+ *
The returned {@link Commands} are not updated when available commands change. Use {@link
+ * Listener#onAvailableCommandsChanged(Commands)} to get an update when the available commands
+ * change.
+ *
+ *
Executing a command that is not available (for example, calling {@link #next()} if {@link
+ * #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} is unavailable) will neither throw an exception nor generate
+ * a {@link #getPlayerError()} player error}.
+ *
+ *
{@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} and {@link #COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM}
+ * are unavailable if there is no such {@link MediaItem}.
+ *
+ * @return The currently available {@link Commands}.
+ * @see Listener#onAvailableCommandsChanged
+ */
+ Commands getAvailableCommands();
+
/** Prepares the player. */
void prepare();
@@ -1133,7 +1247,7 @@ public interface Player {
* Returns the current {@link State playback state} of the player.
*
* @return The current {@link State playback state}.
- * @see EventListener#onPlaybackStateChanged(int)
+ * @see Listener#onPlaybackStateChanged(int)
*/
@State
int getPlaybackState();
@@ -1143,13 +1257,13 @@ public interface Player {
* true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed.
*
* @return The current {@link PlaybackSuppressionReason playback suppression reason}.
- * @see EventListener#onPlaybackSuppressionReasonChanged(int)
+ * @see Listener#onPlaybackSuppressionReasonChanged(int)
*/
@PlaybackSuppressionReason
int getPlaybackSuppressionReason();
/**
- * Returns whether the player is playing, i.e. {@link #getContentPosition()} is advancing.
+ * Returns whether the player is playing, i.e. {@link #getCurrentPosition()} is advancing.
*
*
If {@code false}, then at least one of the following is true:
*
@@ -1160,20 +1274,20 @@ public interface Player {
*
*
* @return Whether the player is playing.
- * @see EventListener#onIsPlayingChanged(boolean)
+ * @see Listener#onIsPlayingChanged(boolean)
*/
boolean isPlaying();
/**
* Returns the error that caused playback to fail. This is the same error that will have been
- * reported via {@link Player.EventListener#onPlayerError(ExoPlaybackException)} at the time of
- * failure. It can be queried using this method until the player is re-prepared.
+ * reported via {@link Listener#onPlayerError(ExoPlaybackException)} at the time of failure. It
+ * can be queried using this method until the player is re-prepared.
*
*
Note that this method will always return {@code null} if {@link #getPlaybackState()} is not
* {@link #STATE_IDLE}.
*
* @return The error, or {@code null}.
- * @see EventListener#onPlayerError(ExoPlaybackException)
+ * @see Listener#onPlayerError(ExoPlaybackException)
*/
@Nullable
ExoPlaybackException getPlayerError();
@@ -1205,7 +1319,7 @@ public interface Player {
* Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
*
* @return Whether playback will proceed when ready.
- * @see EventListener#onPlayWhenReadyChanged(boolean, int)
+ * @see Listener#onPlayWhenReadyChanged(boolean, int)
*/
boolean getPlayWhenReady();
@@ -1220,7 +1334,7 @@ public interface Player {
* Returns the current {@link RepeatMode} used for playback.
*
* @return The current repeat mode.
- * @see EventListener#onRepeatModeChanged(int)
+ * @see Listener#onRepeatModeChanged(int)
*/
@RepeatMode
int getRepeatMode();
@@ -1235,7 +1349,7 @@ public interface Player {
/**
* Returns whether shuffling of windows is enabled.
*
- * @see EventListener#onShuffleModeEnabledChanged(boolean)
+ * @see Listener#onShuffleModeEnabledChanged(boolean)
*/
boolean getShuffleModeEnabled();
@@ -1243,7 +1357,7 @@ public interface Player {
* Whether the player is currently loading the source.
*
* @return Whether the player is currently loading the source.
- * @see EventListener#onIsLoadingChanged(boolean)
+ * @see Listener#onIsLoadingChanged(boolean)
*/
boolean isLoading();
@@ -1327,21 +1441,32 @@ public interface Player {
void next();
/**
- * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the
- * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment.
+ * Attempts to set the playback parameters. Passing {@link PlaybackParameters#DEFAULT} resets the
+ * player to the default, which means there is no speed or pitch adjustment.
*
*
Playback parameters changes may cause the player to buffer. {@link
- * EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the
- * currently active playback parameters change.
+ * Listener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the currently
+ * active playback parameters change.
*
- * @param playbackParameters The playback parameters, or {@code null} to use the defaults.
+ * @param playbackParameters The playback parameters.
*/
- void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters);
+ void setPlaybackParameters(PlaybackParameters playbackParameters);
+
+ /**
+ * Changes the rate at which playback occurs. The pitch is not changed.
+ *
+ *
This is equivalent to {@code
+ * setPlaybackParameters(getPlaybackParameters().withSpeed(speed))}.
+ *
+ * @param speed The linear factor by which playback will be sped up. Must be higher than 0. 1 is
+ * normal speed, 2 is twice as fast, 0.5 is half normal speed...
+ */
+ void setPlaybackSpeed(float speed);
/**
* Returns the currently active playback parameters.
*
- * @see EventListener#onPlaybackParametersChanged(PlaybackParameters)
+ * @see Listener#onPlaybackParametersChanged(PlaybackParameters)
*/
PlaybackParameters getPlaybackParameters();
@@ -1372,33 +1497,21 @@ public interface Player {
*/
void release();
- /** Returns the number of renderers. */
- int getRendererCount();
-
- /**
- * Returns the track type that the renderer at a given index handles.
- *
- *
For example, a video renderer will return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will
- * return {@link C#TRACK_TYPE_AUDIO} and a text renderer will return {@link C#TRACK_TYPE_TEXT}.
- *
- * @param index The index of the renderer.
- * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
- */
- int getRendererType(int index);
-
/**
* Returns the available track groups.
*
- * @see EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
+ * @see Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
*/
TrackGroupArray getCurrentTrackGroups();
/**
- * Returns the current track selections for each renderer.
+ * Returns the current track selections.
*
*
A concrete implementation may include null elements if it has a fixed number of renderer
* components, wishes to report a TrackSelection for each of them, and has one or more renderer
* components that is not assigned any selected tracks.
+ *
+ * @see Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
*/
TrackSelectionArray getCurrentTrackSelections();
@@ -1413,10 +1526,20 @@ public interface Player {
*
This metadata is considered static in that it comes from the tracks' declared Formats,
* rather than being timed (or dynamic) metadata, which is represented within a metadata track.
*
- * @see EventListener#onStaticMetadataChanged(List)
+ * @see Listener#onStaticMetadataChanged(List)
*/
List getCurrentStaticMetadata();
+ /**
+ * Returns the current combined {@link MediaMetadata}, or {@link MediaMetadata#EMPTY} if not
+ * supported.
+ *
+ *
This {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata} and the
+ * static and dynamic metadata sourced from {@link Listener#onStaticMetadataChanged(List)} and
+ * {@link MetadataOutput#onMetadata(Metadata)}.
+ */
+ MediaMetadata getMediaMetadata();
+
/**
* Returns the current manifest. The type depends on the type of media being played. May be null.
*/
@@ -1426,7 +1549,7 @@ public interface Player {
/**
* Returns the current {@link Timeline}. Never null, but may be empty.
*
- * @see EventListener#onTimelineChanged(Timeline, int)
+ * @see Listener#onTimelineChanged(Timeline, int)
*/
Timeline getCurrentTimeline();
@@ -1474,7 +1597,7 @@ public interface Player {
* Returns the media item of the current window in the timeline. May be null if the timeline is
* empty.
*
- * @see EventListener#onMediaItemTransition(MediaItem, int)
+ * @see Listener#onMediaItemTransition(MediaItem, int)
*/
@Nullable
MediaItem getCurrentMediaItem();
@@ -1587,4 +1710,146 @@ public interface Player {
* playing, the returned position is the same as that returned by {@link #getBufferedPosition()}.
*/
long getContentBufferedPosition();
+
+ /** Returns the attributes for audio playback. */
+ AudioAttributes getAudioAttributes();
+
+ /**
+ * Sets the audio volume, with 0 being silence and 1 being unity gain (signal unchanged).
+ *
+ * @param audioVolume Linear output gain to apply to all audio channels.
+ */
+ void setVolume(float audioVolume);
+
+ /**
+ * Returns the audio volume, with 0 being silence and 1 being unity gain (signal unchanged).
+ *
+ * @return The linear gain applied to all audio channels.
+ */
+ float getVolume();
+
+ /**
+ * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView}
+ * currently set on the player.
+ */
+ void clearVideoSurface();
+
+ /**
+ * Clears the {@link Surface} onto which video is being rendered if it matches the one passed.
+ * Else does nothing.
+ *
+ * @param surface The surface to clear.
+ */
+ void clearVideoSurface(@Nullable Surface surface);
+
+ /**
+ * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for
+ * tracking the lifecycle of the surface, and must clear the surface by calling {@code
+ * setVideoSurface(null)} if the surface is destroyed.
+ *
+ *
If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link
+ * SurfaceHolder} then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, {@link
+ * #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} rather than
+ * this method, since passing the holder allows the player to track the lifecycle of the surface
+ * automatically.
+ *
+ * @param surface The {@link Surface}.
+ */
+ void setVideoSurface(@Nullable Surface surface);
+
+ /**
+ * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be
+ * rendered. The player will track the lifecycle of the surface automatically.
+ *
+ * @param surfaceHolder The surface holder.
+ */
+ void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder);
+
+ /**
+ * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being
+ * rendered if it matches the one passed. Else does nothing.
+ *
+ * @param surfaceHolder The surface holder to clear.
+ */
+ void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder);
+
+ /**
+ * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the
+ * lifecycle of the surface automatically.
+ *
+ * @param surfaceView The surface view.
+ */
+ void setVideoSurfaceView(@Nullable SurfaceView surfaceView);
+
+ /**
+ * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one passed.
+ * Else does nothing.
+ *
+ * @param surfaceView The texture view to clear.
+ */
+ void clearVideoSurfaceView(@Nullable SurfaceView surfaceView);
+
+ /**
+ * Sets the {@link TextureView} onto which video will be rendered. The player will track the
+ * lifecycle of the surface automatically.
+ *
+ * @param textureView The texture view.
+ */
+ void setVideoTextureView(@Nullable TextureView textureView);
+
+ /**
+ * Clears the {@link TextureView} onto which video is being rendered if it matches the one passed.
+ * Else does nothing.
+ *
+ * @param textureView The texture view to clear.
+ */
+ void clearVideoTextureView(@Nullable TextureView textureView);
+
+ /**
+ * Gets the size of the video.
+ *
+ *
The video's width and height are {@code 0} if there is no video or its size has not been
+ * determined yet.
+ *
+ * @see Listener#onVideoSizeChanged(VideoSize)
+ */
+ VideoSize getVideoSize();
+
+ /** Returns the current {@link Cue Cues}. This list may be empty. */
+ List getCurrentCues();
+
+ /** Gets the device information. */
+ DeviceInfo getDeviceInfo();
+
+ /**
+ * Gets the current volume of the device.
+ *
+ *
For devices with {@link DeviceInfo#PLAYBACK_TYPE_LOCAL local playback}, the volume returned
+ * by this method varies according to the current {@link C.StreamType stream type}. The stream
+ * type is determined by {@link AudioAttributes#usage} which can be converted to stream type with
+ * {@link Util#getStreamTypeForAudioUsage(int)}.
+ *
+ *
For devices with {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote playback}, the volume of the
+ * remote device is returned.
+ */
+ int getDeviceVolume();
+
+ /** Gets whether the device is muted or not. */
+ boolean isDeviceMuted();
+
+ /**
+ * Sets the volume of the device.
+ *
+ * @param volume The volume to set.
+ */
+ void setDeviceVolume(int volume);
+
+ /** Increases the volume of the device. */
+ void increaseDeviceVolume();
+
+ /** Decreases the volume of the device. */
+ void decreaseDeviceVolume();
+
+ /** Sets the mute state of the device. */
+ void setDeviceMuted(boolean muted);
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Rating.java b/library/common/src/main/java/com/google/android/exoplayer2/Rating.java
new file mode 100644
index 0000000000..0477bbce0e
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/Rating.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 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;
+
+import android.os.Bundle;
+import androidx.annotation.IntDef;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A rating for media content. The style of a rating can be one of {@link HeartRating}, {@link
+ * PercentageRating}, {@link StarRating}, or {@link ThumbRating}.
+ */
+public abstract class Rating implements Bundleable {
+
+ /** A float value that denotes the rating is unset. */
+ public static final float RATING_UNSET = -1.0f;
+
+ // Default package-private constructor to prevent extending Rating class outside this package.
+ /* package */ Rating() {}
+
+ /** Whether the rating exists or not. */
+ public abstract boolean isRated();
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ RATING_TYPE_DEFAULT,
+ RATING_TYPE_HEART,
+ RATING_TYPE_PERCENTAGE,
+ RATING_TYPE_STAR,
+ RATING_TYPE_THUMB
+ })
+ /* package */ @interface RatingType {}
+
+ /* package */ static final int RATING_TYPE_DEFAULT = -1;
+ /* package */ static final int RATING_TYPE_HEART = 0;
+ /* package */ static final int RATING_TYPE_PERCENTAGE = 1;
+ /* package */ static final int RATING_TYPE_STAR = 2;
+ /* package */ static final int RATING_TYPE_THUMB = 3;
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FIELD_RATING_TYPE})
+ private @interface FieldNumber {}
+
+ /* package */ static final int FIELD_RATING_TYPE = 0;
+
+ /** Object that can restore a {@link Rating} from a {@link Bundle}. */
+ public static final Creator CREATOR = Rating::fromBundle;
+
+ private static Rating fromBundle(Bundle bundle) {
+ @RatingType
+ int ratingType =
+ bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_DEFAULT);
+ switch (ratingType) {
+ case RATING_TYPE_HEART:
+ return HeartRating.CREATOR.fromBundle(bundle);
+ case RATING_TYPE_PERCENTAGE:
+ return PercentageRating.CREATOR.fromBundle(bundle);
+ case RATING_TYPE_STAR:
+ return StarRating.CREATOR.fromBundle(bundle);
+ case RATING_TYPE_THUMB:
+ return ThumbRating.CREATOR.fromBundle(bundle);
+ default:
+ throw new IllegalArgumentException("Encountered unknown rating type: " + ratingType);
+ }
+ }
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
+}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/StarRating.java b/library/common/src/main/java/com/google/android/exoplayer2/StarRating.java
new file mode 100644
index 0000000000..543228185e
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/StarRating.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 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;
+
+import static com.google.android.exoplayer2.util.Assertions.checkArgument;
+
+import android.os.Bundle;
+import androidx.annotation.FloatRange;
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.Nullable;
+import com.google.common.base.Objects;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** A rating expressed as a fractional number of stars. */
+public final class StarRating extends Rating {
+
+ @IntRange(from = 1)
+ private final int maxStars;
+
+ private final float starRating;
+
+ /**
+ * Creates a unrated instance with {@code maxStars}. If {@code maxStars} is not a positive
+ * integer, it will throw an {@link IllegalArgumentException}.
+ *
+ * @param maxStars The maximum number of stars this rating can have.
+ */
+ public StarRating(@IntRange(from = 1) int maxStars) {
+ checkArgument(maxStars > 0, "maxStars must be a positive integer");
+ this.maxStars = maxStars;
+ starRating = RATING_UNSET;
+ }
+
+ /**
+ * Creates a rated instance with {@code maxStars} and the given fractional number of stars.
+ * Non-integer values may be used to represent an average rating value. If {@code maxStars} is not
+ * a positive integer or {@code starRating} is out of range, it will throw an {@link
+ * IllegalArgumentException}.
+ *
+ * @param maxStars The maximum number of stars this rating can have.
+ * @param starRating A fractional number of stars of this rating from {@code 0f} to {@code
+ * maxStars}.
+ */
+ public StarRating(@IntRange(from = 1) int maxStars, @FloatRange(from = 0.0) float starRating) {
+ checkArgument(maxStars > 0, "maxStars must be a positive integer");
+ checkArgument(
+ starRating >= 0.0f && starRating <= maxStars, "starRating is out of range [0, maxStars]");
+ this.maxStars = maxStars;
+ this.starRating = starRating;
+ }
+
+ @Override
+ public boolean isRated() {
+ return starRating != RATING_UNSET;
+ }
+
+ /** Returns the maximum number of stars. Must be a positive number. */
+ @IntRange(from = 1)
+ public int getMaxStars() {
+ return maxStars;
+ }
+
+ /**
+ * Returns the fractional number of stars of this rating. Will range from {@code 0f} to {@link
+ * #maxStars}, or {@link #RATING_UNSET} if unrated.
+ */
+ public float getStarRating() {
+ return starRating;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(maxStars, starRating);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof StarRating)) {
+ return false;
+ }
+ StarRating other = (StarRating) obj;
+ return maxStars == other.maxStars && starRating == other.starRating;
+ }
+
+ // Bundleable implementation.
+
+ @RatingType private static final int TYPE = RATING_TYPE_STAR;
+ private static final int MAX_STARS_DEFAULT = 5;
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FIELD_RATING_TYPE, FIELD_MAX_STARS, FIELD_STAR_RATING})
+ private @interface FieldNumber {}
+
+ private static final int FIELD_MAX_STARS = 1;
+ private static final int FIELD_STAR_RATING = 2;
+
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE);
+ bundle.putInt(keyForField(FIELD_MAX_STARS), maxStars);
+ bundle.putFloat(keyForField(FIELD_STAR_RATING), starRating);
+ return bundle;
+ }
+
+ /** Object that can restore a {@link StarRating} from a {@link Bundle}. */
+ public static final Creator CREATOR = StarRating::fromBundle;
+
+ private static StarRating fromBundle(Bundle bundle) {
+ checkArgument(
+ bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_DEFAULT)
+ == TYPE);
+ int maxStars =
+ bundle.getInt(keyForField(FIELD_MAX_STARS), /* defaultValue= */ MAX_STARS_DEFAULT);
+ float starRating =
+ bundle.getFloat(keyForField(FIELD_STAR_RATING), /* defaultValue= */ RATING_UNSET);
+ return starRating == RATING_UNSET
+ ? new StarRating(maxStars)
+ : new StarRating(maxStars, starRating);
+ }
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
+}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ThumbRating.java b/library/common/src/main/java/com/google/android/exoplayer2/ThumbRating.java
new file mode 100644
index 0000000000..07e0ee38d3
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/ThumbRating.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 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;
+
+import static com.google.android.exoplayer2.util.Assertions.checkArgument;
+
+import android.os.Bundle;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import com.google.common.base.Objects;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** A rating expressed as "thumbs up" or "thumbs down". */
+public final class ThumbRating extends Rating {
+
+ private final boolean rated;
+ private final boolean isThumbsUp;
+
+ /** Creates a unrated instance. */
+ public ThumbRating() {
+ rated = false;
+ isThumbsUp = false;
+ }
+
+ /**
+ * Creates a rated instance.
+ *
+ * @param isThumbsUp {@code true} for "thumbs up", {@code false} for "thumbs down".
+ */
+ public ThumbRating(boolean isThumbsUp) {
+ rated = true;
+ this.isThumbsUp = isThumbsUp;
+ }
+
+ @Override
+ public boolean isRated() {
+ return rated;
+ }
+
+ /** Returns whether the rating is "thumbs up". */
+ public boolean isThumbsUp() {
+ return isThumbsUp;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(rated, isThumbsUp);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof ThumbRating)) {
+ return false;
+ }
+ ThumbRating other = (ThumbRating) obj;
+ return isThumbsUp == other.isThumbsUp && rated == other.rated;
+ }
+
+ // Bundleable implementation.
+
+ @RatingType private static final int TYPE = RATING_TYPE_THUMB;
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FIELD_RATING_TYPE, FIELD_RATED, FIELD_IS_THUMBS_UP})
+ private @interface FieldNumber {}
+
+ private static final int FIELD_RATED = 1;
+ private static final int FIELD_IS_THUMBS_UP = 2;
+
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE);
+ bundle.putBoolean(keyForField(FIELD_RATED), rated);
+ bundle.putBoolean(keyForField(FIELD_IS_THUMBS_UP), isThumbsUp);
+ return bundle;
+ }
+
+ /** Object that can restore a {@link ThumbRating} from a {@link Bundle}. */
+ public static final Creator CREATOR = ThumbRating::fromBundle;
+
+ private static ThumbRating fromBundle(Bundle bundle) {
+ checkArgument(
+ bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_DEFAULT)
+ == TYPE);
+ boolean rated = bundle.getBoolean(keyForField(FIELD_RATED), /* defaultValue= */ false);
+ return rated
+ ? new ThumbRating(
+ bundle.getBoolean(keyForField(FIELD_IS_THUMBS_UP), /* defaultValue= */ false))
+ : new ThumbRating();
+ }
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
+}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java
index d7e1e955db..10e91fc22f 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java
@@ -15,15 +15,26 @@
*/
package com.google.android.exoplayer2;
+import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
import android.os.SystemClock;
import android.util.Pair;
+import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.BundleUtil;
import com.google.android.exoplayer2.util.Util;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
/**
* A flexible representation of the structure of media. A timeline is able to represent the
@@ -119,7 +130,7 @@ import com.google.android.exoplayer2.util.Util;
*
This case includes mid-roll ad groups, which are defined as part of the timeline's single
* period. The period can be queried for information about the ad groups and the ads they contain.
*/
-public abstract class Timeline {
+public abstract class Timeline implements Bundleable {
/**
* Holds information about a window in a {@link Timeline}. A window usually corresponds to one
@@ -131,13 +142,15 @@ public abstract class Timeline {
*
*/
- public static final class Window {
+ public static final class Window implements Bundleable {
/**
* A {@link #uid} for a window that must be used for single-window {@link Timeline Timelines}.
*/
public static final Object SINGLE_WINDOW_UID = new Object();
+ private static final Object FAKE_WINDOW_UID = new Object();
+
private static final MediaItem EMPTY_MEDIA_ITEM =
new MediaItem.Builder()
.setMediaId("com.google.android.exoplayer2.Timeline")
@@ -208,14 +221,6 @@ public abstract class Timeline {
*/
public boolean isPlaceholder;
- /** The index of the first period that belongs to this window. */
- public int firstPeriodIndex;
-
- /**
- * The index of the last period that belongs to this window.
- */
- public int lastPeriodIndex;
-
/**
* The default position relative to the start of the window at which to begin playback, in
* microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
@@ -229,6 +234,12 @@ public abstract class Timeline {
*/
public long durationUs;
+ /** The index of the first period that belongs to this window. */
+ public int firstPeriodIndex;
+
+ /** The index of the last period that belongs to this window. */
+ public int lastPeriodIndex;
+
/**
* The position of the start of this window relative to the start of the first period belonging
* to it, in microseconds.
@@ -399,6 +410,142 @@ public abstract class Timeline {
result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32));
return result;
}
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_MEDIA_ITEM,
+ FIELD_PRESENTATION_START_TIME_MS,
+ FIELD_WINDOW_START_TIME_MS,
+ FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS,
+ FIELD_IS_SEEKABLE,
+ FIELD_IS_DYNAMIC,
+ FIELD_LIVE_CONFIGURATION,
+ FIELD_IS_PLACEHOLDER,
+ FIELD_DEFAULT_POSITION_US,
+ FIELD_DURATION_US,
+ FIELD_FIRST_PERIOD_INDEX,
+ FIELD_LAST_PERIOD_INDEX,
+ FIELD_POSITION_IN_FIRST_PERIOD_US,
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_MEDIA_ITEM = 1;
+ private static final int FIELD_PRESENTATION_START_TIME_MS = 2;
+ private static final int FIELD_WINDOW_START_TIME_MS = 3;
+ private static final int FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS = 4;
+ private static final int FIELD_IS_SEEKABLE = 5;
+ private static final int FIELD_IS_DYNAMIC = 6;
+ private static final int FIELD_LIVE_CONFIGURATION = 7;
+ private static final int FIELD_IS_PLACEHOLDER = 8;
+ private static final int FIELD_DEFAULT_POSITION_US = 9;
+ private static final int FIELD_DURATION_US = 10;
+ private static final int FIELD_FIRST_PERIOD_INDEX = 11;
+ private static final int FIELD_LAST_PERIOD_INDEX = 12;
+ private static final int FIELD_POSITION_IN_FIRST_PERIOD_US = 13;
+
+ /**
+ * {@inheritDoc}
+ *
+ *
It omits the {@link #uid} and {@link #manifest} fields. The {@link #uid} of an instance
+ * restored by {@link #CREATOR} will be a fake {@link Object} and the {@link #manifest} of the
+ * instance will be {@code null}.
+ */
+ // TODO(b/166765820): See if missing fields would be okay and add them to the Bundle otherwise.
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(keyForField(FIELD_MEDIA_ITEM), mediaItem.toBundle());
+ bundle.putLong(keyForField(FIELD_PRESENTATION_START_TIME_MS), presentationStartTimeMs);
+ bundle.putLong(keyForField(FIELD_WINDOW_START_TIME_MS), windowStartTimeMs);
+ bundle.putLong(
+ keyForField(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS), elapsedRealtimeEpochOffsetMs);
+ bundle.putBoolean(keyForField(FIELD_IS_SEEKABLE), isSeekable);
+ bundle.putBoolean(keyForField(FIELD_IS_DYNAMIC), isDynamic);
+ @Nullable MediaItem.LiveConfiguration liveConfiguration = this.liveConfiguration;
+ if (liveConfiguration != null) {
+ bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle());
+ }
+ bundle.putBoolean(keyForField(FIELD_IS_PLACEHOLDER), isPlaceholder);
+ bundle.putLong(keyForField(FIELD_DEFAULT_POSITION_US), defaultPositionUs);
+ bundle.putLong(keyForField(FIELD_DURATION_US), durationUs);
+ bundle.putInt(keyForField(FIELD_FIRST_PERIOD_INDEX), firstPeriodIndex);
+ bundle.putInt(keyForField(FIELD_LAST_PERIOD_INDEX), lastPeriodIndex);
+ bundle.putLong(keyForField(FIELD_POSITION_IN_FIRST_PERIOD_US), positionInFirstPeriodUs);
+ return bundle;
+ }
+
+ /**
+ * Object that can restore {@link Period} from a {@link Bundle}.
+ *
+ *
*/
- public static final class Period {
+ public static final class Period implements Bundleable {
/**
* An identifier for the period. Not necessarily unique. May be null if the ids of the period
@@ -435,7 +582,19 @@ public abstract class Timeline {
*/
public long durationUs;
- private long positionInWindowUs;
+ /**
+ * The position of the start of this period relative to the start of the window to which it
+ * belongs, in microseconds. May be negative if the start of the period is not within the
+ * window.
+ */
+ public long positionInWindowUs;
+
+ /**
+ * Whether this period contains placeholder information because the real information has yet to
+ * be loaded.
+ */
+ public boolean isPlaceholder;
+
private AdPlaybackState adPlaybackState;
/** Creates a new instance with no ad playback state. */
@@ -464,7 +623,14 @@ public abstract class Timeline {
int windowIndex,
long durationUs,
long positionInWindowUs) {
- return set(id, uid, windowIndex, durationUs, positionInWindowUs, AdPlaybackState.NONE);
+ return set(
+ id,
+ uid,
+ windowIndex,
+ durationUs,
+ positionInWindowUs,
+ AdPlaybackState.NONE,
+ /* isPlaceholder= */ false);
}
/**
@@ -482,6 +648,8 @@ public abstract class Timeline {
* period is not within the window.
* @param adPlaybackState The state of the period's ads, or {@link AdPlaybackState#NONE} if
* there are no ads.
+ * @param isPlaceholder Whether this period contains placeholder information because the real
+ * information has yet to be loaded.
* @return This period, for convenience.
*/
public Period set(
@@ -490,13 +658,15 @@ public abstract class Timeline {
int windowIndex,
long durationUs,
long positionInWindowUs,
- AdPlaybackState adPlaybackState) {
+ AdPlaybackState adPlaybackState,
+ boolean isPlaceholder) {
this.id = id;
this.uid = uid;
this.windowIndex = windowIndex;
this.durationUs = durationUs;
this.positionInWindowUs = positionInWindowUs;
this.adPlaybackState = adPlaybackState;
+ this.isPlaceholder = isPlaceholder;
return this;
}
@@ -592,9 +762,10 @@ public abstract class Timeline {
}
/**
- * Returns the index of the ad group at or before {@code positionUs} in the period, if that ad
- * group is unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code
- * positionUs} has no ads remaining to be played, or if there is no such ad group.
+ * Returns the index of the ad group at or before {@code positionUs} in the period that should
+ * be played before the content at {@code positionUs}. Returns {@link C#INDEX_UNSET} if the ad
+ * group at or before {@code positionUs} has no ads remaining to be played, or if there is no
+ * such ad group.
*
* @param positionUs The period position at or before which to find an ad group, in
* microseconds.
@@ -606,7 +777,7 @@ public abstract class Timeline {
/**
* Returns the index of the next ad group after {@code positionUs} in the period that has ads
- * remaining to be played. Returns {@link C#INDEX_UNSET} if there is no such ad group.
+ * that should be played. Returns {@link C#INDEX_UNSET} if there is no such ad group.
*
* @param positionUs The period position after which to find an ad group, in microseconds.
* @return The index of the ad group, or {@link C#INDEX_UNSET}.
@@ -661,6 +832,7 @@ public abstract class Timeline {
&& windowIndex == that.windowIndex
&& durationUs == that.durationUs
&& positionInWindowUs == that.positionInWindowUs
+ && isPlaceholder == that.isPlaceholder
&& Util.areEqual(adPlaybackState, that.adPlaybackState);
}
@@ -672,9 +844,84 @@ public abstract class Timeline {
result = 31 * result + windowIndex;
result = 31 * result + (int) (durationUs ^ (durationUs >>> 32));
result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32));
+ result = 31 * result + (isPlaceholder ? 1 : 0);
result = 31 * result + adPlaybackState.hashCode();
return result;
}
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_WINDOW_INDEX,
+ FIELD_DURATION_US,
+ FIELD_POSITION_IN_WINDOW_US,
+ FIELD_PLACEHOLDER,
+ FIELD_AD_PLAYBACK_STATE
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_WINDOW_INDEX = 0;
+ private static final int FIELD_DURATION_US = 1;
+ private static final int FIELD_POSITION_IN_WINDOW_US = 2;
+ private static final int FIELD_PLACEHOLDER = 3;
+ private static final int FIELD_AD_PLAYBACK_STATE = 4;
+
+ /**
+ * {@inheritDoc}
+ *
+ *
It omits the {@link #id} and {@link #uid} fields so these fields of an instance restored
+ * by {@link #CREATOR} will always be {@code null}.
+ */
+ // TODO(b/166765820): See if missing fields would be okay and add them to the Bundle otherwise.
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(keyForField(FIELD_WINDOW_INDEX), windowIndex);
+ bundle.putLong(keyForField(FIELD_DURATION_US), durationUs);
+ bundle.putLong(keyForField(FIELD_POSITION_IN_WINDOW_US), positionInWindowUs);
+ bundle.putBoolean(keyForField(FIELD_PLACEHOLDER), isPlaceholder);
+ bundle.putBundle(keyForField(FIELD_AD_PLAYBACK_STATE), adPlaybackState.toBundle());
+ return bundle;
+ }
+
+ /**
+ * Object that can restore {@link Period} from a {@link Bundle}.
+ *
+ *
The {@link #id} and {@link #uid} of restored instances will always be {@code null}.
+ */
+ public static final Creator CREATOR = Period::fromBundle;
+
+ private static Period fromBundle(Bundle bundle) {
+ int windowIndex = bundle.getInt(keyForField(FIELD_WINDOW_INDEX), /* defaultValue= */ 0);
+ long durationUs =
+ bundle.getLong(keyForField(FIELD_DURATION_US), /* defaultValue= */ C.TIME_UNSET);
+ long positionInWindowUs =
+ bundle.getLong(keyForField(FIELD_POSITION_IN_WINDOW_US), /* defaultValue= */ 0);
+ boolean isPlaceholder = bundle.getBoolean(keyForField(FIELD_PLACEHOLDER));
+ @Nullable
+ Bundle adPlaybackStateBundle = bundle.getBundle(keyForField(FIELD_AD_PLAYBACK_STATE));
+ AdPlaybackState adPlaybackState =
+ adPlaybackStateBundle != null
+ ? AdPlaybackState.CREATOR.fromBundle(adPlaybackStateBundle)
+ : AdPlaybackState.NONE;
+
+ Period period = new Period();
+ period.set(
+ /* id= */ null,
+ /* uid= */ null,
+ windowIndex,
+ durationUs,
+ positionInWindowUs,
+ adPlaybackState,
+ isPlaceholder);
+ return period;
+ }
+
+ private static String keyForField(@Period.FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
}
/** An empty timeline. */
@@ -922,13 +1169,14 @@ public abstract class Timeline {
}
}
int periodIndex = window.firstPeriodIndex;
- long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs;
- long periodDurationUs = getPeriod(periodIndex, period, /* setIds= */ true).getDurationUs();
- while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
- && periodIndex < window.lastPeriodIndex) {
- periodPositionUs -= periodDurationUs;
- periodDurationUs = getPeriod(++periodIndex, period, /* setIds= */ true).getDurationUs();
+ getPeriod(periodIndex, period);
+ while (periodIndex < window.lastPeriodIndex
+ && period.positionInWindowUs != windowPositionUs
+ && getPeriod(periodIndex + 1, period).positionInWindowUs <= windowPositionUs) {
+ periodIndex++;
}
+ getPeriod(periodIndex, period, /* setIds= */ true);
+ long periodPositionUs = windowPositionUs - period.positionInWindowUs;
return Pair.create(Assertions.checkNotNull(period.uid), periodPositionUs);
}
@@ -1029,4 +1277,243 @@ public abstract class Timeline {
}
return result;
}
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_WINDOWS,
+ FIELD_PERIODS,
+ FIELD_SHUFFLED_WINDOW_INDICES,
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_WINDOWS = 0;
+ private static final int FIELD_PERIODS = 1;
+ private static final int FIELD_SHUFFLED_WINDOW_INDICES = 2;
+
+ /**
+ * {@inheritDoc}
+ *
+ *
The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of
+ * an instance restored by {@link #CREATOR} may have missing fields as described in {@link
+ * Window#toBundle()} and {@link Period#toBundle()}.
+ */
+ @Override
+ public final Bundle toBundle() {
+ List windowBundles = new ArrayList<>();
+ int windowCount = getWindowCount();
+ Window window = new Window();
+ for (int i = 0; i < windowCount; i++) {
+ windowBundles.add(getWindow(i, window, /* defaultPositionProjectionUs= */ 0).toBundle());
+ }
+
+ List periodBundles = new ArrayList<>();
+ int periodCount = getPeriodCount();
+ Period period = new Period();
+ for (int i = 0; i < periodCount; i++) {
+ periodBundles.add(getPeriod(i, period, /* setIds= */ false).toBundle());
+ }
+
+ int[] shuffledWindowIndices = new int[windowCount];
+ if (windowCount > 0) {
+ shuffledWindowIndices[0] = getFirstWindowIndex(/* shuffleModeEnabled= */ true);
+ }
+ for (int i = 1; i < windowCount; i++) {
+ shuffledWindowIndices[i] =
+ getNextWindowIndex(
+ shuffledWindowIndices[i - 1], Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true);
+ }
+
+ Bundle bundle = new Bundle();
+ BundleUtil.putBinder(
+ bundle, keyForField(FIELD_WINDOWS), new BundleListRetriever(windowBundles));
+ BundleUtil.putBinder(
+ bundle, keyForField(FIELD_PERIODS), new BundleListRetriever(periodBundles));
+ bundle.putIntArray(keyForField(FIELD_SHUFFLED_WINDOW_INDICES), shuffledWindowIndices);
+ return bundle;
+ }
+
+ /**
+ * Object that can restore a {@link Timeline} from a {@link Bundle}.
+ *
+ *
The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of
+ * a restored instance may have missing fields as described in {@link Window#CREATOR} and {@link
+ * Period#CREATOR}.
+ */
+ public static final Creator CREATOR = Timeline::fromBundle;
+
+ private static Timeline fromBundle(Bundle bundle) {
+ ImmutableList windows =
+ fromBundleListRetriever(
+ Window.CREATOR, BundleUtil.getBinder(bundle, keyForField(FIELD_WINDOWS)));
+ ImmutableList periods =
+ fromBundleListRetriever(
+ Period.CREATOR, BundleUtil.getBinder(bundle, keyForField(FIELD_PERIODS)));
+ @Nullable
+ int[] shuffledWindowIndices = bundle.getIntArray(keyForField(FIELD_SHUFFLED_WINDOW_INDICES));
+ return new RemotableTimeline(
+ windows,
+ periods,
+ shuffledWindowIndices == null
+ ? generateUnshuffledIndices(windows.size())
+ : shuffledWindowIndices);
+ }
+
+ private static ImmutableList fromBundleListRetriever(
+ Creator creator, @Nullable IBinder binder) {
+ if (binder == null) {
+ return ImmutableList.of();
+ }
+ ImmutableList.Builder builder = new ImmutableList.Builder<>();
+ List bundleList = BundleListRetriever.getList(binder);
+ for (int i = 0; i < bundleList.size(); i++) {
+ builder.add(creator.fromBundle(bundleList.get(i)));
+ }
+ return builder.build();
+ }
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
+
+ private static int[] generateUnshuffledIndices(int n) {
+ int[] indices = new int[n];
+ for (int i = 0; i < n; i++) {
+ indices[i] = i;
+ }
+ return indices;
+ }
+
+ /**
+ * A concrete class of {@link Timeline} to restore a {@link Timeline} instance from a {@link
+ * Bundle} sent by another process via {@link IBinder}.
+ */
+ private static final class RemotableTimeline extends Timeline {
+
+ private final ImmutableList windows;
+ private final ImmutableList periods;
+ private final int[] shuffledWindowIndices;
+ private final int[] windowIndicesInShuffled;
+
+ public RemotableTimeline(
+ ImmutableList windows, ImmutableList periods, int[] shuffledWindowIndices) {
+ checkArgument(windows.size() == shuffledWindowIndices.length);
+ this.windows = windows;
+ this.periods = periods;
+ this.shuffledWindowIndices = shuffledWindowIndices;
+ windowIndicesInShuffled = new int[shuffledWindowIndices.length];
+ for (int i = 0; i < shuffledWindowIndices.length; i++) {
+ windowIndicesInShuffled[shuffledWindowIndices[i]] = i;
+ }
+ }
+
+ @Override
+ public int getWindowCount() {
+ return windows.size();
+ }
+
+ @Override
+ public Window getWindow(
+ int windowIndex, Window window, long ignoredDefaultPositionProjectionUs) {
+ Window w = windows.get(windowIndex);
+ window.set(
+ w.uid,
+ w.mediaItem,
+ w.manifest,
+ w.presentationStartTimeMs,
+ w.windowStartTimeMs,
+ w.elapsedRealtimeEpochOffsetMs,
+ w.isSeekable,
+ w.isDynamic,
+ w.liveConfiguration,
+ w.defaultPositionUs,
+ w.durationUs,
+ w.firstPeriodIndex,
+ w.lastPeriodIndex,
+ w.positionInFirstPeriodUs);
+ window.isPlaceholder = w.isPlaceholder;
+ return window;
+ }
+
+ @Override
+ public int getNextWindowIndex(
+ int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
+ if (repeatMode == Player.REPEAT_MODE_ONE) {
+ return windowIndex;
+ }
+ if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) {
+ return repeatMode == Player.REPEAT_MODE_ALL
+ ? getFirstWindowIndex(shuffleModeEnabled)
+ : C.INDEX_UNSET;
+ }
+ return shuffleModeEnabled
+ ? shuffledWindowIndices[windowIndicesInShuffled[windowIndex] + 1]
+ : windowIndex + 1;
+ }
+
+ @Override
+ public int getPreviousWindowIndex(
+ int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
+ if (repeatMode == Player.REPEAT_MODE_ONE) {
+ return windowIndex;
+ }
+ if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) {
+ return repeatMode == Player.REPEAT_MODE_ALL
+ ? getLastWindowIndex(shuffleModeEnabled)
+ : C.INDEX_UNSET;
+ }
+ return shuffleModeEnabled
+ ? shuffledWindowIndices[windowIndicesInShuffled[windowIndex] - 1]
+ : windowIndex - 1;
+ }
+
+ @Override
+ public int getLastWindowIndex(boolean shuffleModeEnabled) {
+ if (isEmpty()) {
+ return C.INDEX_UNSET;
+ }
+ return shuffleModeEnabled
+ ? shuffledWindowIndices[getWindowCount() - 1]
+ : getWindowCount() - 1;
+ }
+
+ @Override
+ public int getFirstWindowIndex(boolean shuffleModeEnabled) {
+ if (isEmpty()) {
+ return C.INDEX_UNSET;
+ }
+ return shuffleModeEnabled ? shuffledWindowIndices[0] : 0;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return periods.size();
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean ignoredSetIds) {
+ Period p = periods.get(periodIndex);
+ period.set(
+ p.id,
+ p.uid,
+ p.windowIndex,
+ p.durationUs,
+ p.positionInWindowUs,
+ p.adPlaybackState,
+ p.isPlaceholder);
+ return period;
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object getUidOfPeriod(int periodIndex) {
+ throw new UnsupportedOperationException();
+ }
+ }
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
index 71ffb00982..b91e642d14 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
@@ -15,10 +15,16 @@
*/
package com.google.android.exoplayer2.audio;
+import android.os.Bundle;
+import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import com.google.android.exoplayer2.Bundleable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* Attributes for audio playback, which configure the underlying platform {@link
@@ -31,7 +37,7 @@ import com.google.android.exoplayer2.util.Util;
*
This class is based on {@link android.media.AudioAttributes}, but can be used on all supported
* API versions.
*/
-public final class AudioAttributes {
+public final class AudioAttributes implements Bundleable {
public static final AudioAttributes DEFAULT = new Builder().build();
@@ -159,4 +165,48 @@ public final class AudioAttributes {
return result;
}
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FIELD_CONTENT_TYPE, FIELD_FLAGS, FIELD_USAGE, FIELD_ALLOWED_CAPTURE_POLICY})
+ private @interface FieldNumber {}
+
+ private static final int FIELD_CONTENT_TYPE = 0;
+ private static final int FIELD_FLAGS = 1;
+ private static final int FIELD_USAGE = 2;
+ private static final int FIELD_ALLOWED_CAPTURE_POLICY = 3;
+
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(keyForField(FIELD_CONTENT_TYPE), contentType);
+ bundle.putInt(keyForField(FIELD_FLAGS), flags);
+ bundle.putInt(keyForField(FIELD_USAGE), usage);
+ bundle.putInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY), allowedCapturePolicy);
+ return bundle;
+ }
+
+ /** Object that can restore {@link AudioAttributes} from a {@link Bundle}. */
+ public static final Creator CREATOR =
+ bundle -> {
+ Builder builder = new Builder();
+ if (bundle.containsKey(keyForField(FIELD_CONTENT_TYPE))) {
+ builder.setContentType(bundle.getInt(keyForField(FIELD_CONTENT_TYPE)));
+ }
+ if (bundle.containsKey(keyForField(FIELD_FLAGS))) {
+ builder.setFlags(bundle.getInt(keyForField(FIELD_FLAGS)));
+ }
+ if (bundle.containsKey(keyForField(FIELD_USAGE))) {
+ builder.setUsage(bundle.getInt(keyForField(FIELD_USAGE)));
+ }
+ if (bundle.containsKey(keyForField(FIELD_ALLOWED_CAPTURE_POLICY))) {
+ builder.setAllowedCapturePolicy(bundle.getInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY)));
+ }
+ return builder.build();
+ };
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioListener.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioListener.java
index 1abe6b2f3c..99e83e3060 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioListener.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioListener.java
@@ -15,7 +15,14 @@
*/
package com.google.android.exoplayer2.audio;
-/** A listener for changes in audio configuration. */
+import com.google.android.exoplayer2.Player;
+
+/**
+ * A listener for changes in audio configuration.
+ *
+ * @deprecated Use {@link Player.Listener}.
+ */
+@Deprecated
public interface AudioListener {
/**
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java
index 8640c46e1a..3a0f0e093d 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java
@@ -23,9 +23,7 @@ import com.google.android.exoplayer2.util.ParsableBitArray;
import java.nio.ByteBuffer;
import java.util.Arrays;
-/**
- * Utility methods for parsing DTS frames.
- */
+/** Utility methods for parsing DTS frames. */
public final class DtsUtil {
/**
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java b/library/common/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java
index 8fd25f2cf9..6d74bf820e 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java
@@ -17,9 +17,7 @@ package com.google.android.exoplayer2.decoder;
import com.google.android.exoplayer2.C;
-/**
- * Base class for buffers with flags.
- */
+/** Base class for buffers with flags. */
public abstract class Buffer {
@C.BufferFlags
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
index 7eaab6ae1d..80486feb91 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java
@@ -21,9 +21,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
-/**
- * Compatibility wrapper for {@link android.media.MediaCodec.CryptoInfo}.
- */
+/** Compatibility wrapper for {@link android.media.MediaCodec.CryptoInfo}. */
public final class CryptoInfo {
/**
@@ -121,12 +119,6 @@ public final class CryptoInfo {
return frameworkCryptoInfo;
}
- /** @deprecated Use {@link #getFrameworkCryptoInfo()}. */
- @Deprecated
- public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {
- return getFrameworkCryptoInfo();
- }
-
/**
* Increases the number of clear data for the first sub sample by {@code count}.
*
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/library/common/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java
index 4d00ee7748..ff295657e1 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java
@@ -24,9 +24,7 @@ import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
-/**
- * Holds input for a decoder.
- */
+/** Holds input for a decoder. */
public class DecoderInputBuffer extends Buffer {
/**
@@ -111,12 +109,8 @@ public class DecoderInputBuffer extends Buffer {
@BufferReplacementMode private final int bufferReplacementMode;
private final int paddingSize;
- /**
- * Creates a new instance for which {@link #isFlagsOnly()} will return true.
- *
- * @return A new flags only input buffer.
- */
- public static DecoderInputBuffer newFlagsOnlyInstance() {
+ /** Returns a new instance that's not able to hold any data. */
+ public static DecoderInputBuffer newNoDataInstance() {
return new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED);
}
@@ -200,14 +194,6 @@ public class DecoderInputBuffer extends Buffer {
data = newData;
}
- /**
- * Returns whether the buffer is only able to hold flags, meaning {@link #data} is null and
- * its replacement mode is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}.
- */
- public final boolean isFlagsOnly() {
- return data == null && bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DISABLED;
- }
-
/**
* Returns whether the {@link C#BUFFER_FLAG_ENCRYPTED} flag is set.
*/
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java
index 8d662c318e..bfcbcee8ae 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java
@@ -15,8 +15,10 @@
*/
package com.google.android.exoplayer2.device;
+import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.Bundleable;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -24,7 +26,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** Information about the playback device. */
-public final class DeviceInfo {
+public final class DeviceInfo implements Bundleable {
/** Types of playback. One of {@link #PLAYBACK_TYPE_LOCAL} or {@link #PLAYBACK_TYPE_REMOTE}. */
@Documented
@@ -80,4 +82,39 @@ public final class DeviceInfo {
result = 31 * result + maxVolume;
return result;
}
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FIELD_PLAYBACK_TYPE, FIELD_MIN_VOLUME, FIELD_MAX_VOLUME})
+ private @interface FieldNumber {}
+
+ private static final int FIELD_PLAYBACK_TYPE = 0;
+ private static final int FIELD_MIN_VOLUME = 1;
+ private static final int FIELD_MAX_VOLUME = 2;
+
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(keyForField(FIELD_PLAYBACK_TYPE), playbackType);
+ bundle.putInt(keyForField(FIELD_MIN_VOLUME), minVolume);
+ bundle.putInt(keyForField(FIELD_MAX_VOLUME), maxVolume);
+ return bundle;
+ }
+
+ /** Object that can restore {@link DeviceInfo} from a {@link Bundle}. */
+ public static final Creator CREATOR =
+ bundle -> {
+ int playbackType =
+ bundle.getInt(
+ keyForField(FIELD_PLAYBACK_TYPE), /* defaultValue= */ PLAYBACK_TYPE_LOCAL);
+ int minVolume = bundle.getInt(keyForField(FIELD_MIN_VOLUME), /* defaultValue= */ 0);
+ int maxVolume = bundle.getInt(keyForField(FIELD_MAX_VOLUME), /* defaultValue= */ 0);
+ return new DeviceInfo(playbackType, minVolume, maxVolume);
+ };
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java b/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java
index 3d35c6ad54..408868da79 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java
@@ -17,7 +17,12 @@ package com.google.android.exoplayer2.device;
import com.google.android.exoplayer2.Player;
-/** A listener for changes of {@link Player.DeviceComponent}. */
+/**
+ * A listener for changes of {@link DeviceInfo} or device volume.
+ *
+ * @deprecated Use {@link Player.Listener}.
+ */
+@Deprecated
public interface DeviceListener {
/** Called when the device information changes. */
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/common/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
index bc2b8bba86..4113f4c27d 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
@@ -29,9 +29,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.UUID;
-/**
- * Initialization data for one or more DRM schemes.
- */
+/** Initialization data for one or more DRM schemes. */
public final class DrmInitData implements Comparator, Parcelable {
/**
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java
index 21dacd4f9b..01ae340609 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java
@@ -19,13 +19,12 @@ import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.List;
-/**
- * A collection of metadata entries.
- */
+/** A collection of metadata entries. */
public final class Metadata implements Parcelable {
/** A metadata entry. */
@@ -48,6 +47,17 @@ public final class Metadata implements Parcelable {
default byte[] getWrappedMetadataBytes() {
return null;
}
+
+ /**
+ * Updates the {@link MediaMetadata.Builder} with the type specific values stored in this Entry.
+ *
+ *
The order of the {@link Entry} objects in the {@link Metadata} matters. If two {@link
+ * Entry} entries attempt to populate the same {@link MediaMetadata} field, then the last one in
+ * the list is used.
+ *
+ * @param builder The builder to be updated.
+ */
+ default void populateMediaMetadata(MediaMetadata.Builder builder) {}
}
private final Entry[] entries;
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java
index 46501ce002..825f690fe8 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java
@@ -18,9 +18,7 @@ package com.google.android.exoplayer2.metadata;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
-/**
- * Decodes metadata from binary data.
- */
+/** Decodes metadata from binary data. */
public interface MetadataDecoder {
/**
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java
index a09b565653..55e0b75f71 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java
@@ -18,9 +18,7 @@ package com.google.android.exoplayer2.metadata;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
-/**
- * A {@link DecoderInputBuffer} for a {@link MetadataDecoder}.
- */
+/** A {@link DecoderInputBuffer} for a {@link MetadataDecoder}. */
public final class MetadataInputBuffer extends DecoderInputBuffer {
/**
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java
index b635cbc4b2..d203ffd801 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java
@@ -15,9 +15,7 @@
*/
package com.google.android.exoplayer2.metadata;
-/**
- * Receives metadata output.
- */
+/** Receives metadata output. */
public interface MetadataOutput {
/**
@@ -26,5 +24,4 @@ public interface MetadataOutput {
* @param metadata The metadata.
*/
void onMetadata(Metadata metadata);
-
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
index 3f4a400677..4f05cc7f08 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
@@ -23,9 +23,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
-/**
- * APIC (Attached Picture) ID3 frame.
- */
+/** APIC (Attached Picture) ID3 frame. */
public final class ApicFrame extends Id3Frame {
public static final String ID = "APIC";
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
index 6c6057bb7a..995418f3b4 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
@@ -22,9 +22,7 @@ import android.os.Parcelable;
import androidx.annotation.Nullable;
import java.util.Arrays;
-/**
- * Binary ID3 frame.
- */
+/** Binary ID3 frame. */
public final class BinaryFrame extends Id3Frame {
public final byte[] data;
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
index bf5d2de6ea..120b9269f1 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
@@ -23,9 +23,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
-/**
- * Chapter information ID3 frame.
- */
+/** Chapter information ID3 frame. */
public final class ChapterFrame extends Id3Frame {
public static final String ID = "CHAP";
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
index c8aa9bd9ad..5e662c388c 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
@@ -22,9 +22,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
-/**
- * Chapter table of contents ID3 frame.
- */
+/** Chapter table of contents ID3 frame. */
public final class ChapterTocFrame extends Id3Frame {
public static final String ID = "CTOC";
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
index 363057f17a..8b2d14444d 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
@@ -22,9 +22,7 @@ import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
-/**
- * Comment ID3 frame.
- */
+/** Comment ID3 frame. */
public final class CommentFrame extends Id3Frame {
public static final String ID = "COMM";
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
index 6023f76aa1..c0c8ad631f 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
@@ -23,9 +23,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
-/**
- * GEOB (General Encapsulated Object) ID3 frame.
- */
+/** GEOB (General Encapsulated Object) ID3 frame. */
public final class GeobFrame extends Id3Frame {
public static final String ID = "GEOB";
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
index f660e21bfd..5bd0e1e3e8 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
+import com.google.common.base.Ascii;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -539,13 +540,13 @@ public final class Id3Decoder extends SimpleMetadataDecoder {
int mimeTypeEndIndex;
if (majorVersion == 2) {
mimeTypeEndIndex = 2;
- mimeType = "image/" + Util.toLowerInvariant(new String(data, 0, 3, "ISO-8859-1"));
+ mimeType = "image/" + Ascii.toLowerCase(new String(data, 0, 3, "ISO-8859-1"));
if ("image/jpg".equals(mimeType)) {
mimeType = "image/jpeg";
}
} else {
mimeTypeEndIndex = indexOfZeroByte(data, 0);
- mimeType = Util.toLowerInvariant(new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"));
+ mimeType = Ascii.toLowerCase(new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"));
if (mimeType.indexOf('/') == -1) {
mimeType = "image/" + mimeType;
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
index 27ea833deb..24e1188879 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
@@ -17,9 +17,7 @@ package com.google.android.exoplayer2.metadata.id3;
import com.google.android.exoplayer2.metadata.Metadata;
-/**
- * Base class for ID3 frames.
- */
+/** Base class for ID3 frames. */
public abstract class Id3Frame implements Metadata.Entry {
/**
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
index 6e53485453..773e49e846 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
@@ -23,9 +23,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
-/**
- * PRIV (Private) ID3 frame.
- */
+/** PRIV (Private) ID3 frame. */
public final class PrivFrame extends Id3Frame {
public static final String ID = "PRIV";
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
index 8337911c0d..4a36b7afe7 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
@@ -20,11 +20,10 @@ import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.util.Util;
-/**
- * Text information ID3 frame.
- */
+/** Text information ID3 frame. */
public final class TextInformationFrame extends Id3Frame {
@Nullable public final String description;
@@ -42,6 +41,30 @@ public final class TextInformationFrame extends Id3Frame {
value = castNonNull(in.readString());
}
+ @Override
+ public void populateMediaMetadata(MediaMetadata.Builder builder) {
+ switch (id) {
+ case "TT2":
+ case "TIT2":
+ builder.setTitle(value);
+ break;
+ case "TP1":
+ case "TPE1":
+ builder.setArtist(value);
+ break;
+ case "TP2":
+ case "TPE2":
+ builder.setAlbumArtist(value);
+ break;
+ case "TAL":
+ case "TALB":
+ builder.setAlbumTitle(value);
+ break;
+ default:
+ break;
+ }
+ }
+
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
@@ -51,7 +74,8 @@ public final class TextInformationFrame extends Id3Frame {
return false;
}
TextInformationFrame other = (TextInformationFrame) obj;
- return id.equals(other.id) && Util.areEqual(description, other.description)
+ return Util.areEqual(id, other.id)
+ && Util.areEqual(description, other.description)
&& Util.areEqual(value, other.value);
}
@@ -90,7 +114,5 @@ public final class TextInformationFrame extends Id3Frame {
public TextInformationFrame[] newArray(int size) {
return new TextInformationFrame[size];
}
-
};
-
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
index 298558b662..d9b73ab011 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
@@ -22,9 +22,7 @@ import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
-/**
- * Url link ID3 frame.
- */
+/** Url link ID3 frame. */
public final class UrlLinkFrame extends Id3Frame {
@Nullable public final String description;
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/source/MediaPeriodId.java b/library/common/src/main/java/com/google/android/exoplayer2/source/MediaPeriodId.java
index ad0e289ba9..8192486a1b 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/source/MediaPeriodId.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/source/MediaPeriodId.java
@@ -152,6 +152,14 @@ public class MediaPeriodId {
newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, nextAdGroupIndex);
}
+ /** Returns a copy of this period identifier with a new {@code windowSequenceNumber}. */
+ public MediaPeriodId copyWithWindowSequenceNumber(long windowSequenceNumber) {
+ return this.windowSequenceNumber == windowSequenceNumber
+ ? this
+ : new MediaPeriodId(
+ periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, nextAdGroupIndex);
+ }
+
/** Returns whether this period identifier identifies an ad in an ad group in a period. */
public boolean isAd() {
return adGroupIndex != C.INDEX_UNSET;
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/common/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
index 607f797103..2df780ce93 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
@@ -21,11 +21,14 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Log;
import java.util.Arrays;
/** Defines an immutable group of tracks identified by their format identity. */
public final class TrackGroup implements Parcelable {
+ private static final String TAG = "TrackGroup";
+
/** The number of tracks in the group. */
public final int length;
@@ -41,6 +44,7 @@ public final class TrackGroup implements Parcelable {
Assertions.checkState(formats.length > 0);
this.formats = formats;
this.length = formats.length;
+ verifyCorrectness();
}
/* package */ TrackGroup(Parcel in) {
@@ -129,4 +133,62 @@ public final class TrackGroup implements Parcelable {
return new TrackGroup[size];
}
};
+
+ private void verifyCorrectness() {
+ // TrackGroups should only contain tracks with exactly the same content (but in different
+ // qualities). We only log an error instead of throwing to not break backwards-compatibility for
+ // cases where malformed TrackGroups happen to work by chance (e.g. because adaptive selections
+ // are always disabled).
+ String language = normalizeLanguage(formats[0].language);
+ @C.RoleFlags int roleFlags = normalizeRoleFlags(formats[0].roleFlags);
+ for (int i = 1; i < formats.length; i++) {
+ if (!language.equals(normalizeLanguage(formats[i].language))) {
+ logErrorMessage(
+ /* mismatchField= */ "languages",
+ /* valueIndex0= */ formats[0].language,
+ /* otherValue=* */ formats[i].language,
+ /* otherIndex= */ i);
+ return;
+ }
+ if (roleFlags != normalizeRoleFlags(formats[i].roleFlags)) {
+ logErrorMessage(
+ /* mismatchField= */ "role flags",
+ /* valueIndex0= */ Integer.toBinaryString(formats[0].roleFlags),
+ /* otherValue=* */ Integer.toBinaryString(formats[i].roleFlags),
+ /* otherIndex= */ i);
+ return;
+ }
+ }
+ }
+
+ private static String normalizeLanguage(@Nullable String language) {
+ // Treat all variants of undetermined or unknown languages as compatible.
+ return language == null || language.equals(C.LANGUAGE_UNDETERMINED) ? "" : language;
+ }
+
+ @C.RoleFlags
+ private static int normalizeRoleFlags(@C.RoleFlags int roleFlags) {
+ // Treat trick-play and non-trick-play formats as compatible.
+ return roleFlags | C.ROLE_FLAG_TRICK_PLAY;
+ }
+
+ private static void logErrorMessage(
+ String mismatchField,
+ @Nullable String valueIndex0,
+ @Nullable String otherValue,
+ int otherIndex) {
+ Log.e(
+ TAG,
+ "",
+ new IllegalStateException(
+ "Different "
+ + mismatchField
+ + " combined in one TrackGroup: '"
+ + valueIndex0
+ + "' (track 0) and '"
+ + otherValue
+ + "' (track "
+ + otherIndex
+ + ")"));
+ }
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
index b70dd10c38..5b207492d9 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
@@ -15,18 +15,21 @@
*/
package com.google.android.exoplayer2.source.ads;
+import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static java.lang.Math.max;
import android.net.Uri;
+import android.os.Bundle;
import androidx.annotation.CheckResult;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.Bundleable;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.Arrays;
import org.checkerframework.checker.nullness.compatqual.NullableType;
@@ -36,7 +39,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
*
Instances are immutable. Call the {@code with*} methods to get new instances that have the
* required changes.
*/
-public final class AdPlaybackState {
+public final class AdPlaybackState implements Bundleable {
/**
* Represents a group of ads, with information about their states.
@@ -44,7 +47,7 @@ public final class AdPlaybackState {
*
Instances are immutable. Call the {@code with*} methods to get new instances that have the
* required changes.
*/
- public static final class AdGroup {
+ public static final class AdGroup implements Bundleable {
/** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */
public final int count;
@@ -66,7 +69,7 @@ public final class AdPlaybackState {
private AdGroup(
int count, @AdState int[] states, @NullableType Uri[] uris, long[] durationsUs) {
- Assertions.checkArgument(states.length == uris.length);
+ checkArgument(states.length == uris.length);
this.count = count;
this.states = states;
this.uris = uris;
@@ -162,9 +165,9 @@ public final class AdPlaybackState {
*/
@CheckResult
public AdGroup withAdState(@AdState int state, int index) {
- Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);
+ checkArgument(count == C.LENGTH_UNSET || index < count);
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
- Assertions.checkArgument(
+ checkArgument(
states[index] == AD_STATE_UNAVAILABLE
|| states[index] == AD_STATE_AVAILABLE
|| states[index] == state);
@@ -230,6 +233,55 @@ public final class AdPlaybackState {
Arrays.fill(durationsUs, oldDurationsUsCount, newDurationsUsCount, C.TIME_UNSET);
return durationsUs;
}
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FIELD_COUNT, FIELD_URIS, FIELD_STATES, FIELD_DURATIONS_US})
+ private @interface FieldNumber {}
+
+ private static final int FIELD_COUNT = 0;
+ private static final int FIELD_URIS = 1;
+ private static final int FIELD_STATES = 2;
+ private static final int FIELD_DURATIONS_US = 3;
+
+ // putParcelableArrayList actually supports null elements.
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(keyForField(FIELD_COUNT), count);
+ bundle.putParcelableArrayList(
+ keyForField(FIELD_URIS), new ArrayList<@NullableType Uri>(Arrays.asList(uris)));
+ bundle.putIntArray(keyForField(FIELD_STATES), states);
+ bundle.putLongArray(keyForField(FIELD_DURATIONS_US), durationsUs);
+ return bundle;
+ }
+
+ /** Object that can restore {@link AdGroup} from a {@link Bundle}. */
+ public static final Creator CREATOR = AdGroup::fromBundle;
+
+ // getParcelableArrayList may have null elements.
+ @SuppressWarnings("nullness:type.argument.type.incompatible")
+ private static AdGroup fromBundle(Bundle bundle) {
+ int count = bundle.getInt(keyForField(FIELD_COUNT), /* defaultValue= */ C.LENGTH_UNSET);
+ @Nullable
+ ArrayList<@NullableType Uri> uriList = bundle.getParcelableArrayList(keyForField(FIELD_URIS));
+ @Nullable
+ @AdState
+ int[] states = bundle.getIntArray(keyForField(FIELD_STATES));
+ @Nullable long[] durationsUs = bundle.getLongArray(keyForField(FIELD_DURATIONS_US));
+ return new AdGroup(
+ count,
+ states == null ? new int[0] : states,
+ uriList == null ? new Uri[0] : uriList.toArray(new Uri[0]),
+ durationsUs == null ? new long[0] : durationsUs);
+ }
+
+ private static String keyForField(@AdGroup.FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
}
/**
@@ -312,6 +364,7 @@ public final class AdPlaybackState {
@Nullable AdGroup[] adGroups,
long adResumePositionUs,
long contentDurationUs) {
+ checkArgument(adGroups == null || adGroups.length == adGroupTimesUs.length);
this.adsId = adsId;
this.adGroupTimesUs = adGroupTimesUs;
this.adResumePositionUs = adResumePositionUs;
@@ -327,9 +380,9 @@ public final class AdPlaybackState {
}
/**
- * Returns the index of the ad group at or before {@code positionUs}, if that ad group is
- * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no
- * ads remaining to be played, or if there is no such ad group.
+ * Returns the index of the ad group at or before {@code positionUs} that should be played before
+ * the content at {@code positionUs}. Returns {@link C#INDEX_UNSET} if the ad group at or before
+ * {@code positionUs} has no ads remaining to be played, or if there is no such ad group.
*
* @param positionUs The period position at or before which to find an ad group, in microseconds,
* or {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any
@@ -349,8 +402,8 @@ public final class AdPlaybackState {
}
/**
- * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be
- * played. Returns {@link C#INDEX_UNSET} if there is no such ad group.
+ * Returns the index of the next ad group after {@code positionUs} that should be played. Returns
+ * {@link C#INDEX_UNSET} if there is no such ad group.
*
* @param positionUs The period position after which to find an ad group, in microseconds, or
* {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad
@@ -368,8 +421,8 @@ public final class AdPlaybackState {
// In practice we expect there to be few ad groups so the search shouldn't be expensive.
int index = 0;
while (index < adGroupTimesUs.length
- && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE
- && (positionUs >= adGroupTimesUs[index] || !adGroups[index].hasUnplayedAds())) {
+ && ((adGroupTimesUs[index] != C.TIME_END_OF_SOURCE && adGroupTimesUs[index] <= positionUs)
+ || !adGroups[index].hasUnplayedAds())) {
index++;
}
return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
@@ -393,7 +446,7 @@ public final class AdPlaybackState {
*/
@CheckResult
public AdPlaybackState withAdCount(int adGroupIndex, int adCount) {
- Assertions.checkArgument(adCount > 0);
+ checkArgument(adCount > 0);
if (adGroups[adGroupIndex].count == adCount) {
return this;
}
@@ -578,4 +631,79 @@ public final class AdPlaybackState {
return positionUs < adGroupPositionUs;
}
}
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_AD_GROUP_TIMES_US,
+ FIELD_AD_GROUPS,
+ FIELD_AD_RESUME_POSITION_US,
+ FIELD_CONTENT_DURATION_US
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_AD_GROUP_TIMES_US = 1;
+ private static final int FIELD_AD_GROUPS = 2;
+ private static final int FIELD_AD_RESUME_POSITION_US = 3;
+ private static final int FIELD_CONTENT_DURATION_US = 4;
+
+ /**
+ * {@inheritDoc}
+ *
+ *
It omits the {@link #adsId} field so the {@link #adsId} of instances restored by {@link
+ * #CREATOR} will always be {@code null}.
+ */
+ // TODO(b/166765820): See if missing adsId would be okay and add adsId to the Bundle otherwise.
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putLongArray(keyForField(FIELD_AD_GROUP_TIMES_US), adGroupTimesUs);
+ ArrayList adGroupBundleList = new ArrayList<>();
+ for (AdGroup adGroup : adGroups) {
+ adGroupBundleList.add(adGroup.toBundle());
+ }
+ bundle.putParcelableArrayList(keyForField(FIELD_AD_GROUPS), adGroupBundleList);
+ bundle.putLong(keyForField(FIELD_AD_RESUME_POSITION_US), adResumePositionUs);
+ bundle.putLong(keyForField(FIELD_CONTENT_DURATION_US), contentDurationUs);
+ return bundle;
+ }
+
+ /**
+ * Object that can restore {@link AdPlaybackState} from a {@link Bundle}.
+ *
+ *
The {@link #adsId} of restored instances will always be {@code null}.
+ */
+ public static final Bundleable.Creator CREATOR = AdPlaybackState::fromBundle;
+
+ private static AdPlaybackState fromBundle(Bundle bundle) {
+ @Nullable long[] adGroupTimesUs = bundle.getLongArray(keyForField(FIELD_AD_GROUP_TIMES_US));
+ @Nullable
+ ArrayList adGroupBundleList =
+ bundle.getParcelableArrayList(keyForField(FIELD_AD_GROUPS));
+ @Nullable AdGroup[] adGroups;
+ if (adGroupBundleList == null) {
+ adGroups = null;
+ } else {
+ adGroups = new AdGroup[adGroupBundleList.size()];
+ for (int i = 0; i < adGroupBundleList.size(); i++) {
+ adGroups[i] = AdGroup.CREATOR.fromBundle(adGroupBundleList.get(i));
+ }
+ }
+ long adResumePositionUs =
+ bundle.getLong(keyForField(FIELD_AD_RESUME_POSITION_US), /* defaultValue= */ 0);
+ long contentDurationUs =
+ bundle.getLong(keyForField(FIELD_CONTENT_DURATION_US), /* defaultValue= */ C.TIME_UNSET);
+ return new AdPlaybackState(
+ /* adsId= */ null,
+ adGroupTimesUs == null ? new long[0] : adGroupTimesUs,
+ adGroups,
+ adResumePositionUs,
+ contentDurationUs);
+ }
+
+ private static String keyForField(@FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java
index 49a45e1b22..46f865782f 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java
@@ -140,6 +140,12 @@ public final class Cue {
/** The alignment of the cue text within the cue box, or null if the alignment is undefined. */
@Nullable public final Alignment textAlignment;
+ /**
+ * The alignment of multiple lines of text relative to the longest line, or null if the alignment
+ * is undefined.
+ */
+ @Nullable public final Alignment multiRowAlignment;
+
/** The cue image, or null if this is a text cue. */
@Nullable public final Bitmap bitmap;
@@ -364,6 +370,7 @@ public final class Cue {
this(
text,
textAlignment,
+ /* multiRowAlignment= */ null,
/* bitmap= */ null,
line,
lineType,
@@ -410,6 +417,7 @@ public final class Cue {
this(
text,
textAlignment,
+ /* multiRowAlignment= */ null,
/* bitmap= */ null,
line,
lineType,
@@ -429,6 +437,7 @@ public final class Cue {
private Cue(
@Nullable CharSequence text,
@Nullable Alignment textAlignment,
+ @Nullable Alignment multiRowAlignment,
@Nullable Bitmap bitmap,
float line,
@LineType int lineType,
@@ -451,6 +460,7 @@ public final class Cue {
}
this.text = text;
this.textAlignment = textAlignment;
+ this.multiRowAlignment = multiRowAlignment;
this.bitmap = bitmap;
this.line = line;
this.lineType = lineType;
@@ -477,6 +487,7 @@ public final class Cue {
@Nullable private CharSequence text;
@Nullable private Bitmap bitmap;
@Nullable private Alignment textAlignment;
+ @Nullable private Alignment multiRowAlignment;
private float line;
@LineType private int lineType;
@AnchorType private int lineAnchor;
@@ -495,6 +506,7 @@ public final class Cue {
text = null;
bitmap = null;
textAlignment = null;
+ multiRowAlignment = null;
line = DIMEN_UNSET;
lineType = TYPE_UNSET;
lineAnchor = TYPE_UNSET;
@@ -513,6 +525,7 @@ public final class Cue {
text = cue.text;
bitmap = cue.bitmap;
textAlignment = cue.textAlignment;
+ multiRowAlignment = cue.multiRowAlignment;
line = cue.line;
lineType = cue.lineType;
lineAnchor = cue.lineAnchor;
@@ -592,6 +605,18 @@ public final class Cue {
return textAlignment;
}
+ /**
+ * Sets the multi-row alignment of the cue.
+ *
+ *
Passing null means the alignment is undefined.
+ *
+ * @see Cue#multiRowAlignment
+ */
+ public Builder setMultiRowAlignment(@Nullable Layout.Alignment multiRowAlignment) {
+ this.multiRowAlignment = multiRowAlignment;
+ return this;
+ }
+
/**
* Sets the position of the cue box within the viewport in the direction orthogonal to the
* writing direction.
@@ -827,6 +852,7 @@ public final class Cue {
return new Cue(
text,
textAlignment,
+ multiRowAlignment,
bitmap,
line,
lineType,
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/text/TextOutput.java b/library/common/src/main/java/com/google/android/exoplayer2/text/TextOutput.java
index a039255fa9..12c781c5f9 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/text/TextOutput.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/text/TextOutput.java
@@ -17,14 +17,15 @@ package com.google.android.exoplayer2.text;
import java.util.List;
-/**
- * Receives text output.
- */
+/** Receives text output. */
public interface TextOutput {
/**
* Called when there is a change in the {@link Cue Cues}.
*
+ *
{@code cues} is in ascending order of priority. If any of the cue boxes overlap when
+ * displayed, the {@link Cue} nearer the end of the list should be shown on top.
+ *
* @param cues The {@link Cue Cues}. May be empty.
*/
void onCues(List cues);
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java
index dca840790d..e439b5d00e 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java
@@ -27,6 +27,21 @@ import com.google.android.exoplayer2.source.TrackGroup;
*/
public interface TrackSelection {
+ /** An unspecified track selection type. */
+ int TYPE_UNSET = 0;
+ /** The first value that can be used for application specific track selection types. */
+ int TYPE_CUSTOM_BASE = 10000;
+
+ /**
+ * Returns an integer specifying the type of the selection, or {@link #TYPE_UNSET} if not
+ * specified.
+ *
+ *
Track selection types are specific to individual applications, but should be defined
+ * starting from {@link #TYPE_CUSTOM_BASE} to ensure they don't conflict with any types that may
+ * be added to the library in the future.
+ */
+ int getType();
+
/** Returns the {@link TrackGroup} to which the selected tracks belong. */
TrackGroup getTrackGroup();
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ui/AdOverlayInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ui/AdOverlayInfo.java
new file mode 100644
index 0000000000..b3eee8970a
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/ui/AdOverlayInfo.java
@@ -0,0 +1,74 @@
+/*
+ * 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.ui;
+
+import android.view.View;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Provides information about an overlay view shown on top of an ad view group. */
+public final class AdOverlayInfo {
+
+ /**
+ * The purpose of the overlay. One of {@link #PURPOSE_CONTROLS}, {@link #PURPOSE_CLOSE_AD}, {@link
+ * #PURPOSE_OTHER} or {@link #PURPOSE_NOT_VISIBLE}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({PURPOSE_CONTROLS, PURPOSE_CLOSE_AD, PURPOSE_OTHER, PURPOSE_NOT_VISIBLE})
+ public @interface Purpose {}
+ /** Purpose for playback controls overlaying the player. */
+ public static final int PURPOSE_CONTROLS = 0;
+ /** Purpose for ad close buttons overlaying the player. */
+ public static final int PURPOSE_CLOSE_AD = 1;
+ /** Purpose for other overlays. */
+ public static final int PURPOSE_OTHER = 2;
+ /** Purpose for overlays that are not visible. */
+ public static final int PURPOSE_NOT_VISIBLE = 3;
+
+ /** The overlay view. */
+ public final View view;
+ /** The purpose of the overlay view. */
+ @Purpose public final int purpose;
+ /** An optional, detailed reason that the overlay view is needed. */
+ @Nullable public final String reasonDetail;
+
+ /**
+ * Creates a new overlay info.
+ *
+ * @param view The view that is overlaying the player.
+ * @param purpose The purpose of the view.
+ */
+ public AdOverlayInfo(View view, @Purpose int purpose) {
+ this(view, purpose, /* detailedReason= */ null);
+ }
+
+ /**
+ * Creates a new overlay info.
+ *
+ * @param view The view that is overlaying the player.
+ * @param purpose The purpose of the view.
+ * @param detailedReason An optional, detailed reason that the view is on top of the player.
+ */
+ public AdOverlayInfo(View view, @Purpose int purpose, @Nullable String detailedReason) {
+ this.view = view;
+ this.purpose = purpose;
+ this.reasonDetail = detailedReason;
+ }
+}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ui/AdViewProvider.java b/library/common/src/main/java/com/google/android/exoplayer2/ui/AdViewProvider.java
new file mode 100644
index 0000000000..dd6fa84184
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/ui/AdViewProvider.java
@@ -0,0 +1,48 @@
+/*
+ * 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.ui;
+
+import android.view.ViewGroup;
+import androidx.annotation.Nullable;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+/** Provides information about views for the ad playback UI. */
+public interface AdViewProvider {
+
+ /**
+ * Returns the {@link ViewGroup} on top of the player that will show any ad UI, or {@code null} if
+ * playing audio-only ads. Any views on top of the returned view group must be described by {@link
+ * AdOverlayInfo AdOverlayInfos} returned by {@link #getAdOverlayInfos()}, for accurate
+ * viewability measurement.
+ */
+ @Nullable
+ ViewGroup getAdViewGroup();
+
+ /**
+ * Returns a list of {@link AdOverlayInfo} instances describing views that are on top of the ad
+ * view group, but that are essential for controlling playback and should be excluded from ad
+ * viewability measurements.
+ *
+ *
Each view must be either a fully transparent overlay (for capturing touch events), or a
+ * small piece of transient UI that is essential to the user experience of playback (such as a
+ * button to pause/resume playback or a transient full-screen or cast button). For more
+ * information see the documentation for your ads loader.
+ */
+ default List getAdOverlayInfos() {
+ return ImmutableList.of();
+ }
+}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/package-info.java b/library/common/src/main/java/com/google/android/exoplayer2/ui/package-info.java
similarity index 100%
rename from library/ui/src/main/java/com/google/android/exoplayer2/ui/package-info.java
rename to library/common/src/main/java/com/google/android/exoplayer2/ui/package-info.java
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java
index bbc182d7af..c157002809 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java
@@ -45,17 +45,31 @@ public interface DataSource extends DataReader {
void addTransferListener(TransferListener transferListener);
/**
- * Opens the source to read the specified data.
- *
- * Note: If an {@link IOException} is thrown, callers must still call {@link #close()} to ensure
- * that any partial effects of the invocation are cleaned up.
+ * Opens the source to read the specified data. If an {@link IOException} is thrown, callers must
+ * still call {@link #close()} to ensure that any partial effects of the invocation are cleaned
+ * up.
+ *
+ *
The following edge case behaviors apply:
+ *
+ *
+ *
If the {@link DataSpec#position requested position} is within the resource, but the
+ * {@link DataSpec#length requested length} extends beyond the end of the resource, then
+ * {@link #open} will succeed and data from the requested position to the end of the
+ * resource will be made available through {@link #read}.
+ *
If the {@link DataSpec#position requested position} is equal to the length of the
+ * resource, then {@link #open} will succeed, and {@link #read} will immediately return
+ * {@link C#RESULT_END_OF_INPUT}.
+ *
If the {@link DataSpec#position requested position} is greater than the length of the
+ * resource, then {@link #open} will throw an {@link IOException} for which {@link
+ * DataSourceException#isCausedByPositionOutOfRange} will be {@code true}.
+ *
*
* @param dataSpec Defines the data to be read.
* @throws IOException If an error occurs opening the source. {@link DataSourceException} can be
* thrown or used as a cause of the thrown exception to specify the reason of the error.
* @return The number of bytes that can be read from the opened source. For unbounded requests
- * (i.e. requests where {@link DataSpec#length} equals {@link C#LENGTH_UNSET}) this value
- * is the resolved length of the request, or {@link C#LENGTH_UNSET} if the length is still
+ * (i.e., requests where {@link DataSpec#length} equals {@link C#LENGTH_UNSET}) this value is
+ * the resolved length of the request, or {@link C#LENGTH_UNSET} if the length is still
* unresolved. For all other requests, the value returned will be equal to the request's
* {@link DataSpec#length}.
*/
@@ -82,10 +96,8 @@ public interface DataSource extends DataReader {
}
/**
- * Closes the source.
- *
- * Note: This method must be called even if the corresponding call to {@link #open(DataSpec)}
- * threw an {@link IOException}. See {@link #open(DataSpec)} for more details.
+ * Closes the source. This method must be called even if the corresponding call to {@link
+ * #open(DataSpec)} threw an {@link IOException}.
*
* @throws IOException If an error occurs closing the source.
*/
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java
index a45b7db2f2..c3ccdb88d9 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DataSourceException.java
@@ -18,9 +18,7 @@ package com.google.android.exoplayer2.upstream;
import androidx.annotation.Nullable;
import java.io.IOException;
-/**
- * Used to specify reason of a DataSource error.
- */
+/** Used to specify reason of a DataSource error. */
public final class DataSourceException extends IOException {
/**
@@ -41,6 +39,10 @@ public final class DataSourceException extends IOException {
return false;
}
+ /**
+ * Indicates that the {@link DataSpec#position starting position} of the request was outside the
+ * bounds of the data.
+ */
public static final int POSITION_OUT_OF_RANGE = 0;
/**
@@ -56,5 +58,4 @@ public final class DataSourceException extends IOException {
public DataSourceException(int reason) {
this.reason = reason;
}
-
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
index 575a10b6cd..88193c2646 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
@@ -15,21 +15,21 @@
*/
package com.google.android.exoplayer2.upstream;
+import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
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;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
+import com.google.common.base.Ascii;
import com.google.common.base.Predicate;
-import java.io.EOFException;
+import com.google.common.net.HttpHeaders;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
@@ -43,10 +43,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
-import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
@@ -207,8 +204,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307;
private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308;
private static final long MAX_BYTES_TO_DRAIN = 2048;
- private static final Pattern CONTENT_RANGE_HEADER =
- Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
private final boolean allowCrossProtocolRedirects;
private final int connectTimeoutMillis;
@@ -221,14 +216,9 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
@Nullable private DataSpec dataSpec;
@Nullable private HttpURLConnection connection;
@Nullable private InputStream inputStream;
- private byte @MonotonicNonNull [] skipBuffer;
private boolean opened;
private int responseCode;
-
- private long bytesToSkip;
private long bytesToRead;
-
- private long bytesSkipped;
private long bytesRead;
/** @deprecated Use {@link DefaultHttpDataSource.Factory} instead. */
@@ -341,8 +331,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {
this.dataSpec = dataSpec;
- this.bytesRead = 0;
- this.bytesSkipped = 0;
+ bytesRead = 0;
+ bytesToRead = 0;
transferInitializing(dataSpec);
try {
@@ -350,7 +340,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
} catch (IOException e) {
@Nullable String message = e.getMessage();
if (message != null
- && Util.toLowerInvariant(message).matches("cleartext http traffic.*not permitted.*")) {
+ && Ascii.toLowerCase(message).matches("cleartext http traffic.*not permitted.*")) {
throw new CleartextNotPermittedException(e, dataSpec);
}
throw new HttpDataSourceException(
@@ -371,6 +361,16 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
// Check for a valid response code.
if (responseCode < 200 || responseCode > 299) {
Map> headers = connection.getHeaderFields();
+ if (responseCode == 416) {
+ long documentSize =
+ HttpUtil.getDocumentSize(connection.getHeaderField(HttpHeaders.CONTENT_RANGE));
+ if (dataSpec.position == documentSize) {
+ opened = true;
+ transferStarted(dataSpec);
+ return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : 0;
+ }
+ }
+
@Nullable InputStream errorStream = connection.getErrorStream();
byte[] errorResponseBody;
try {
@@ -383,7 +383,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
InvalidResponseCodeException exception =
new InvalidResponseCodeException(
responseCode, responseMessage, headers, dataSpec, errorResponseBody);
-
if (responseCode == 416) {
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
}
@@ -400,7 +399,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
// If we requested a range starting from a non-zero position and received a 200 rather than a
// 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
- bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
+ long bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Determine the length of the data to be read, after skipping.
boolean isCompressed = isCompressed(connection);
@@ -408,7 +407,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length;
} else {
- long contentLength = getContentLength(connection);
+ long contentLength =
+ HttpUtil.getContentLength(
+ connection.getHeaderField(HttpHeaders.CONTENT_LENGTH),
+ connection.getHeaderField(HttpHeaders.CONTENT_RANGE));
bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip)
: C.LENGTH_UNSET;
}
@@ -432,13 +434,21 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
opened = true;
transferStarted(dataSpec);
+ try {
+ if (!skipFully(bytesToSkip)) {
+ throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
+ }
+ } catch (IOException e) {
+ closeConnectionQuietly();
+ throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN);
+ }
+
return bytesToRead;
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
try {
- skipInternal();
return readInternal(buffer, offset, readLength);
} catch (IOException e) {
throw new HttpDataSourceException(
@@ -451,7 +461,9 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
try {
@Nullable InputStream inputStream = this.inputStream;
if (inputStream != null) {
- maybeTerminateInputStream(connection, bytesRemaining());
+ long bytesRemaining =
+ bytesToRead == C.LENGTH_UNSET ? C.LENGTH_UNSET : bytesToRead - bytesRead;
+ maybeTerminateInputStream(connection, bytesRemaining);
try {
inputStream.close();
} catch (IOException e) {
@@ -469,48 +481,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
}
}
- /**
- * Returns the current connection, or null if the source is not currently opened.
- *
- * @return The current open connection, or null.
- */
- @Nullable
- protected final HttpURLConnection getConnection() {
- return connection;
- }
-
- /**
- * Returns the number of bytes that have been skipped since the most recent call to
- * {@link #open(DataSpec)}.
- *
- * @return The number of bytes skipped.
- */
- protected final long bytesSkipped() {
- return bytesSkipped;
- }
-
- /**
- * Returns the number of bytes that have been read since the most recent call to
- * {@link #open(DataSpec)}.
- *
- * @return The number of bytes read.
- */
- protected final long bytesRead() {
- return bytesRead;
- }
-
- /**
- * Returns the number of bytes that are still to be read for the current {@link DataSpec}.
- *
- * If the total length of the data being read is known, then this length minus {@code bytesRead()}
- * is returned. If the total length is unknown, {@link C#LENGTH_UNSET} is returned.
- *
- * @return The remaining length, or {@link C#LENGTH_UNSET}.
- */
- protected final long bytesRemaining() {
- return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;
- }
-
/**
* Establishes a connection, following redirects to do so where permitted.
*/
@@ -616,17 +586,14 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
connection.setRequestProperty(property.getKey(), property.getValue());
}
- if (!(position == 0 && length == C.LENGTH_UNSET)) {
- String rangeRequest = "bytes=" + position + "-";
- if (length != C.LENGTH_UNSET) {
- rangeRequest += (position + length - 1);
- }
- connection.setRequestProperty("Range", rangeRequest);
+ @Nullable String rangeHeader = buildRangeRequestHeader(position, length);
+ if (rangeHeader != null) {
+ connection.setRequestProperty(HttpHeaders.RANGE, rangeHeader);
}
if (userAgent != null) {
- connection.setRequestProperty("User-Agent", userAgent);
+ connection.setRequestProperty(HttpHeaders.USER_AGENT, userAgent);
}
- connection.setRequestProperty("Accept-Encoding", allowGzip ? "gzip" : "identity");
+ connection.setRequestProperty(HttpHeaders.ACCEPT_ENCODING, allowGzip ? "gzip" : "identity");
connection.setInstanceFollowRedirects(followRedirects);
connection.setDoOutput(httpBody != null);
connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));
@@ -679,80 +646,32 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
}
/**
- * Attempts to extract the length of the content from the response headers of an open connection.
- *
- * @param connection The open connection.
- * @return The extracted length, or {@link C#LENGTH_UNSET}.
- */
- private static long getContentLength(HttpURLConnection connection) {
- long contentLength = C.LENGTH_UNSET;
- String contentLengthHeader = connection.getHeaderField("Content-Length");
- if (!TextUtils.isEmpty(contentLengthHeader)) {
- try {
- contentLength = Long.parseLong(contentLengthHeader);
- } catch (NumberFormatException e) {
- Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
- }
- }
- String contentRangeHeader = connection.getHeaderField("Content-Range");
- if (!TextUtils.isEmpty(contentRangeHeader)) {
- Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader);
- if (matcher.find()) {
- try {
- long contentLengthFromRange =
- Long.parseLong(checkNotNull(matcher.group(2)))
- - Long.parseLong(checkNotNull(matcher.group(1)))
- + 1;
- if (contentLength < 0) {
- // Some proxy servers strip the Content-Length header. Fall back to the length
- // calculated here in this case.
- contentLength = contentLengthFromRange;
- } else if (contentLength != contentLengthFromRange) {
- // If there is a discrepancy between the Content-Length and Content-Range headers,
- // assume the one with the larger value is correct. We have seen cases where carrier
- // change one of them to reduce the size of a request, but it is unlikely anybody would
- // increase it.
- Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
- + "]");
- contentLength = max(contentLength, contentLengthFromRange);
- }
- } catch (NumberFormatException e) {
- Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
- }
- }
- }
- return contentLength;
- }
-
- /**
- * Skips any bytes that need skipping. Else does nothing.
- *
- * This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
+ * Attempts to skip the specified number of bytes in full.
*
+ * @param bytesToSkip The number of bytes to skip.
* @throws InterruptedIOException If the thread is interrupted during the operation.
- * @throws EOFException If the end of the input stream is reached before the bytes are skipped.
+ * @throws IOException If an error occurs reading from the source.
+ * @return Whether the bytes were skipped in full. If {@code false} then the data ended before the
+ * specified number of bytes were skipped. Always {@code true} if {@code bytesToSkip == 0}.
*/
- private void skipInternal() throws IOException {
- if (bytesSkipped == bytesToSkip) {
- return;
+ private boolean skipFully(long bytesToSkip) throws IOException {
+ if (bytesToSkip == 0) {
+ return true;
}
-
- if (skipBuffer == null) {
- skipBuffer = new byte[4096];
- }
-
- while (bytesSkipped != bytesToSkip) {
- int readLength = (int) min(bytesToSkip - bytesSkipped, skipBuffer.length);
+ byte[] skipBuffer = new byte[4096];
+ while (bytesToSkip > 0) {
+ int readLength = (int) min(bytesToSkip, skipBuffer.length);
int read = castNonNull(inputStream).read(skipBuffer, 0, readLength);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException();
}
if (read == -1) {
- throw new EOFException();
+ return false;
}
- bytesSkipped += read;
+ bytesToSkip -= read;
bytesTransferred(read);
}
+ return true;
}
/**
@@ -783,10 +702,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
int read = castNonNull(inputStream).read(buffer, offset, readLength);
if (read == -1) {
- if (bytesToRead != C.LENGTH_UNSET) {
- // End of stream reached having not read sufficient data.
- throw new EOFException();
- }
return C.RESULT_END_OF_INPUT;
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java
index e2ae38b013..dbaa686066 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java
@@ -19,6 +19,7 @@ import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
+import com.google.common.base.Ascii;
import com.google.common.base.Predicate;
import java.io.IOException;
import java.lang.annotation.Documented;
@@ -29,9 +30,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
-/**
- * An HTTP {@link DataSource}.
- */
+/** An HTTP {@link DataSource}. */
public interface HttpDataSource extends DataSource {
/**
@@ -182,7 +181,10 @@ public interface HttpDataSource extends DataSource {
/** A {@link Predicate} that rejects content types often used for pay-walls. */
Predicate REJECT_PAYWALL_TYPES =
contentType -> {
- contentType = Util.toLowerInvariant(contentType);
+ if (contentType == null) {
+ return false;
+ }
+ contentType = Ascii.toLowerCase(contentType);
return !TextUtils.isEmpty(contentType)
&& (!contentType.contains("text") || contentType.contains("text/vtt"))
&& !contentType.contains("html")
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpUtil.java
new file mode 100644
index 0000000000..ac433009a7
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpUtil.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 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.upstream;
+
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+import static java.lang.Math.max;
+
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Log;
+import com.google.common.net.HttpHeaders;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Utility methods for HTTP. */
+public final class HttpUtil {
+
+ private static final String TAG = "HttpUtil";
+ private static final Pattern CONTENT_RANGE_WITH_START_AND_END =
+ Pattern.compile("bytes (\\d+)-(\\d+)/(?:\\d+|\\*)");
+ private static final Pattern CONTENT_RANGE_WITH_SIZE =
+ Pattern.compile("bytes (?:(?:\\d+-\\d+)|\\*)/(\\d+)");
+
+ /** Class only contains static methods. */
+ private HttpUtil() {}
+
+ /**
+ * Builds a {@link HttpHeaders#RANGE Range header} for the given position and length.
+ *
+ * @param position The request position.
+ * @param length The request length, or {@link C#LENGTH_UNSET} if the request is unbounded.
+ * @return The corresponding range header, or {@code null} if a header is unnecessary because the
+ * whole resource is being requested.
+ */
+ @Nullable
+ public static String buildRangeRequestHeader(long position, long length) {
+ if (position == 0 && length == C.LENGTH_UNSET) {
+ return null;
+ }
+ StringBuilder rangeValue = new StringBuilder();
+ rangeValue.append("bytes=");
+ rangeValue.append(position);
+ rangeValue.append("-");
+ if (length != C.LENGTH_UNSET) {
+ rangeValue.append(position + length - 1);
+ }
+ return rangeValue.toString();
+ }
+
+ /**
+ * Attempts to parse the document size from a {@link HttpHeaders#CONTENT_RANGE Content-Range
+ * header}.
+ *
+ * @param contentRangeHeader The {@link HttpHeaders#CONTENT_RANGE Content-Range header}, or {@code
+ * null} if not set.
+ * @return The document size, or {@link C#LENGTH_UNSET} if it could not be determined.
+ */
+ public static long getDocumentSize(@Nullable String contentRangeHeader) {
+ if (TextUtils.isEmpty(contentRangeHeader)) {
+ return C.LENGTH_UNSET;
+ }
+ Matcher matcher = CONTENT_RANGE_WITH_SIZE.matcher(contentRangeHeader);
+ return matcher.matches() ? Long.parseLong(checkNotNull(matcher.group(1))) : C.LENGTH_UNSET;
+ }
+
+ /**
+ * Attempts to parse the length of a response body from the corresponding response headers.
+ *
+ * @param contentLengthHeader The {@link HttpHeaders#CONTENT_LENGTH Content-Length header}, or
+ * {@code null} if not set.
+ * @param contentRangeHeader The {@link HttpHeaders#CONTENT_RANGE Content-Range header}, or {@code
+ * null} if not set.
+ * @return The length of the response body, or {@link C#LENGTH_UNSET} if it could not be
+ * determined.
+ */
+ public static long getContentLength(
+ @Nullable String contentLengthHeader, @Nullable String contentRangeHeader) {
+ long contentLength = C.LENGTH_UNSET;
+ if (!TextUtils.isEmpty(contentLengthHeader)) {
+ try {
+ contentLength = Long.parseLong(contentLengthHeader);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
+ }
+ }
+ if (!TextUtils.isEmpty(contentRangeHeader)) {
+ Matcher matcher = CONTENT_RANGE_WITH_START_AND_END.matcher(contentRangeHeader);
+ if (matcher.matches()) {
+ try {
+ long contentLengthFromRange =
+ Long.parseLong(checkNotNull(matcher.group(2)))
+ - Long.parseLong(checkNotNull(matcher.group(1)))
+ + 1;
+ if (contentLength < 0) {
+ // Some proxy servers strip the Content-Length header. Fall back to the length
+ // calculated here in this case.
+ contentLength = contentLengthFromRange;
+ } else if (contentLength != contentLengthFromRange) {
+ // If there is a discrepancy between the Content-Length and Content-Range headers,
+ // assume the one with the larger value is correct. We have seen cases where carrier
+ // change one of them to reduce the size of a request, but it is unlikely anybody would
+ // increase it.
+ Log.w(
+ TAG,
+ "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader + "]");
+ contentLength = max(contentLength, contentLengthFromRange);
+ }
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
+ }
+ }
+ }
+ return contentLength;
+ }
+}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Assertions.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Assertions.java
index c6173730ff..0bb65e55e0 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/util/Assertions.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Assertions.java
@@ -22,9 +22,7 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.dataflow.qual.Pure;
-/**
- * Provides methods for asserting the truth of expressions and properties.
- */
+/** Provides methods for asserting the truth of expressions and properties. */
public final class Assertions {
private Assertions() {}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/BundleUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/BundleUtil.java
new file mode 100644
index 0000000000..1c1e139d80
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/BundleUtil.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 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.util;
+
+import android.os.Bundle;
+import android.os.IBinder;
+import androidx.annotation.Nullable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/** Utilities for {@link Bundle}. */
+public final class BundleUtil {
+
+ private static final String TAG = "BundleUtil";
+
+ @Nullable private static Method getIBinderMethod;
+ @Nullable private static Method putIBinderMethod;
+
+ /**
+ * Gets an {@link IBinder} inside a {@link Bundle} for all Android versions.
+ *
+ * @param bundle The bundle to get the {@link IBinder}.
+ * @param key The key to use while getting the {@link IBinder}.
+ * @return The {@link IBinder} that was obtained.
+ */
+ @Nullable
+ public static IBinder getBinder(Bundle bundle, @Nullable String key) {
+ if (Util.SDK_INT >= 18) {
+ return bundle.getBinder(key);
+ } else {
+ return getBinderByReflection(bundle, key);
+ }
+ }
+
+ /**
+ * Puts an {@link IBinder} inside a {@link Bundle} for all Android versions.
+ *
+ * @param bundle The bundle to insert the {@link IBinder}.
+ * @param key The key to use while putting the {@link IBinder}.
+ * @param binder The {@link IBinder} to put.
+ */
+ public static void putBinder(Bundle bundle, @Nullable String key, @Nullable IBinder binder) {
+ if (Util.SDK_INT >= 18) {
+ bundle.putBinder(key, binder);
+ } else {
+ putBinderByReflection(bundle, key, binder);
+ }
+ }
+
+ // Method.invoke may take null "key".
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ @Nullable
+ private static IBinder getBinderByReflection(Bundle bundle, @Nullable String key) {
+ @Nullable Method getIBinder = getIBinderMethod;
+ if (getIBinder == null) {
+ try {
+ getIBinderMethod = Bundle.class.getMethod("getIBinder", String.class);
+ getIBinderMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve getIBinder method", e);
+ return null;
+ }
+ getIBinder = getIBinderMethod;
+ }
+
+ try {
+ return (IBinder) getIBinder.invoke(bundle, key);
+ } catch (InvocationTargetException | IllegalAccessException | IllegalArgumentException e) {
+ Log.i(TAG, "Failed to invoke getIBinder via reflection", e);
+ return null;
+ }
+ }
+
+ // Method.invoke may take null "key" and "binder".
+ @SuppressWarnings("nullness:argument.type.incompatible")
+ private static void putBinderByReflection(
+ Bundle bundle, @Nullable String key, @Nullable IBinder binder) {
+ @Nullable Method putIBinder = putIBinderMethod;
+ if (putIBinder == null) {
+ try {
+ putIBinderMethod = Bundle.class.getMethod("putIBinder", String.class, IBinder.class);
+ putIBinderMethod.setAccessible(true);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Failed to retrieve putIBinder method", e);
+ return;
+ }
+ putIBinder = putIBinderMethod;
+ }
+
+ try {
+ putIBinder.invoke(bundle, key, binder);
+ } catch (InvocationTargetException | IllegalAccessException | IllegalArgumentException e) {
+ Log.i(TAG, "Failed to invoke putIBinder via reflection", e);
+ }
+ }
+
+ private BundleUtil() {}
+}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java
index ffb8236bd1..8ecb2ab8ec 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java
@@ -43,9 +43,6 @@ public interface Clock {
/** @see android.os.SystemClock#uptimeMillis() */
long uptimeMillis();
- /** @see android.os.SystemClock#sleep(long) */
- void sleep(long sleepTimeMs);
-
/**
* Creates a {@link HandlerWrapper} using a specified looper and a specified callback for handling
* messages.
@@ -53,4 +50,12 @@ public interface Clock {
* @see Handler#Handler(Looper, Handler.Callback)
*/
HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback);
+
+ /**
+ * Notifies the clock that the current thread is about to be blocked and won't return until a
+ * condition on another thread becomes true.
+ *
+ *
Should be a no-op for all non-test cases.
+ */
+ void onThreadBlocked();
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/CopyOnWriteMultiset.java b/library/common/src/main/java/com/google/android/exoplayer2/util/CopyOnWriteMultiset.java
index 505ff55cbe..c473e2206b 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/util/CopyOnWriteMultiset.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/CopyOnWriteMultiset.java
@@ -138,4 +138,11 @@ public final class CopyOnWriteMultiset implements Iterable
return elements.iterator();
}
}
+
+ /** Returns the number of occurrences of an element in this multiset. */
+ public int count(E element) {
+ synchronized (lock) {
+ return elementCounts.containsKey(element) ? elementCounts.get(element) : 0;
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java
similarity index 100%
rename from library/core/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java
rename to library/common/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/ExoFlags.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ExoFlags.java
new file mode 100644
index 0000000000..46c9e486df
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/ExoFlags.java
@@ -0,0 +1,179 @@
+/*
+ * 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.util;
+
+import static com.google.android.exoplayer2.util.Assertions.checkIndex;
+import static com.google.android.exoplayer2.util.Assertions.checkState;
+
+import android.util.SparseBooleanArray;
+import androidx.annotation.Nullable;
+
+/**
+ * A set of integer flags.
+ *
+ *
Intended for usages where the number of flags may exceed 32 and can no longer be represented
+ * by an IntDef.
+ *
+ *
Instances are immutable.
+ */
+public final class ExoFlags {
+
+ /** A builder for {@link ExoFlags} instances. */
+ public static final class Builder {
+
+ private final SparseBooleanArray flags;
+
+ private boolean buildCalled;
+
+ /** Creates a builder. */
+ public Builder() {
+ flags = new SparseBooleanArray();
+ }
+
+ /**
+ * Adds a flag.
+ *
+ * @param flag A flag.
+ * @return This builder.
+ * @throws IllegalStateException If {@link #build()} has already been called.
+ */
+ public Builder add(int flag) {
+ checkState(!buildCalled);
+ flags.append(flag, /* value= */ true);
+ return this;
+ }
+
+ /**
+ * Adds a flag if the provided condition is true. Does nothing otherwise.
+ *
+ * @param flag A flag.
+ * @param condition A condition.
+ * @return This builder.
+ * @throws IllegalStateException If {@link #build()} has already been called.
+ */
+ public Builder addIf(int flag, boolean condition) {
+ if (condition) {
+ return add(flag);
+ }
+ return this;
+ }
+
+ /**
+ * Adds flags.
+ *
+ * @param flags The flags to add.
+ * @return This builder.
+ * @throws IllegalStateException If {@link #build()} has already been called.
+ */
+ public Builder addAll(int... flags) {
+ for (int flag : flags) {
+ add(flag);
+ }
+ return this;
+ }
+
+ /**
+ * Adds {@link ExoFlags flags}.
+ *
+ * @param flags The set of flags to add.
+ * @return This builder.
+ * @throws IllegalStateException If {@link #build()} has already been called.
+ */
+ public Builder addAll(ExoFlags flags) {
+ for (int i = 0; i < flags.size(); i++) {
+ add(flags.get(i));
+ }
+ return this;
+ }
+
+ /**
+ * Builds an {@link ExoFlags} instance.
+ *
+ * @throws IllegalStateException If this method has already been called.
+ */
+ public ExoFlags build() {
+ checkState(!buildCalled);
+ buildCalled = true;
+ return new ExoFlags(flags);
+ }
+ }
+
+ // A SparseBooleanArray is used instead of a Set to avoid auto-boxing the flag values.
+ private final SparseBooleanArray flags;
+
+ private ExoFlags(SparseBooleanArray flags) {
+ this.flags = flags;
+ }
+
+ /**
+ * Returns whether the set contains the given flag.
+ *
+ * @param flag The flag.
+ * @return Whether the set contains the flag.
+ */
+ public boolean contains(int flag) {
+ return flags.get(flag);
+ }
+
+ /**
+ * Returns whether the set contains at least one of the given flags.
+ *
+ * @param flags The flags.
+ * @return Whether the set contains at least one of the flags.
+ */
+ public boolean containsAny(int... flags) {
+ for (int flag : flags) {
+ if (contains(flag)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Returns the number of flags in this set. */
+ public int size() {
+ return flags.size();
+ }
+
+ /**
+ * Returns the flag at the given index.
+ *
+ * @param index The index. Must be between 0 (inclusive) and {@link #size()} (exclusive).
+ * @return The flag at the given index.
+ * @throws IndexOutOfBoundsException If index is outside the allowed range.
+ */
+ public int get(int index) {
+ checkIndex(index, /* start= */ 0, /* limit= */ size());
+ return flags.keyAt(index);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ExoFlags)) {
+ return false;
+ }
+ ExoFlags that = (ExoFlags) o;
+ return flags.equals(that.flags);
+ }
+
+ @Override
+ public int hashCode() {
+ return flags.hashCode();
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java
similarity index 100%
rename from library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java
rename to library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/common/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java
index edf775bd5b..8247447d93 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java
@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.util;
import android.os.Handler;
import android.os.Looper;
-import android.os.Message;
import androidx.annotation.Nullable;
/**
@@ -26,6 +25,16 @@ import androidx.annotation.Nullable;
*/
public interface HandlerWrapper {
+ /** A message obtained from the handler. */
+ interface Message {
+
+ /** See {@link android.os.Message#sendToTarget()}. */
+ void sendToTarget();
+
+ /** See {@link android.os.Message#getTarget()}. */
+ HandlerWrapper getTarget();
+ }
+
/** See {@link Handler#getLooper()}. */
Looper getLooper();
@@ -44,6 +53,9 @@ public interface HandlerWrapper {
/** See {@link Handler#obtainMessage(int, int, int, Object)}. */
Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj);
+ /** See {@link Handler#sendMessageAtFrontOfQueue(android.os.Message)}. */
+ boolean sendMessageAtFrontOfQueue(Message message);
+
/** See {@link Handler#sendEmptyMessage(int)}. */
boolean sendEmptyMessage(int what);
@@ -64,4 +76,7 @@ public interface HandlerWrapper {
/** See {@link Handler#postDelayed(Runnable, long)}. */
boolean postDelayed(Runnable runnable, long delayMs);
+
+ /** See {@link android.os.Handler#postAtFrontOfQueue(Runnable)}. */
+ boolean postAtFrontOfQueue(Runnable runnable);
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java
index a9a749e47f..fe220b1946 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java
@@ -20,7 +20,6 @@ import android.os.Message;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
-import com.google.common.base.Supplier;
import java.util.ArrayDeque;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nonnull;
@@ -35,9 +34,8 @@ import javax.annotation.Nonnull;
* was enqueued and haven't been removed since.
*
* @param The listener type.
- * @param The {@link MutableFlags} type used to indicate which events occurred.
*/
-public final class ListenerSet {
+public final class ListenerSet {
/**
* An event sent to a listener.
@@ -55,17 +53,17 @@ public final class ListenerSet {
* iteration were handled by the listener.
*
* @param The listener type.
- * @param The {@link MutableFlags} type used to indicate which events occurred.
*/
- public interface IterationFinishedEvent {
+ public interface IterationFinishedEvent {
/**
* Invokes the iteration finished event.
*
* @param listener The listener to invoke the event on.
- * @param eventFlags The combined event flags of all events sent in this iteration.
+ * @param eventFlags The combined event {@link ExoFlags flags} of all events sent in this
+ * iteration.
*/
- void invoke(T listener, E eventFlags);
+ void invoke(T listener, ExoFlags eventFlags);
}
private static final int MSG_ITERATION_FINISHED = 0;
@@ -73,9 +71,8 @@ public final class ListenerSet {
private final Clock clock;
private final HandlerWrapper handler;
- private final Supplier eventFlagsSupplier;
- private final IterationFinishedEvent iterationFinishedEvent;
- private final CopyOnWriteArraySet> listeners;
+ private final IterationFinishedEvent iterationFinishedEvent;
+ private final CopyOnWriteArraySet> listeners;
private final ArrayDeque flushingEvents;
private final ArrayDeque queuedEvents;
@@ -87,33 +84,24 @@ public final class ListenerSet {
* @param looper A {@link Looper} used to call listeners on. The same {@link Looper} must be used
* to call all other methods of this class.
* @param clock A {@link Clock}.
- * @param eventFlagsSupplier A {@link Supplier} for new instances of {@link E the event flags
- * type}.
* @param iterationFinishedEvent An {@link IterationFinishedEvent} sent when all other events sent
* during one {@link Looper} message queue iteration were handled by the listeners.
*/
- public ListenerSet(
- Looper looper,
- Clock clock,
- Supplier eventFlagsSupplier,
- IterationFinishedEvent iterationFinishedEvent) {
+ public ListenerSet(Looper looper, Clock clock, IterationFinishedEvent iterationFinishedEvent) {
this(
/* listeners= */ new CopyOnWriteArraySet<>(),
looper,
clock,
- eventFlagsSupplier,
iterationFinishedEvent);
}
private ListenerSet(
- CopyOnWriteArraySet> listeners,
+ CopyOnWriteArraySet> listeners,
Looper looper,
Clock clock,
- Supplier eventFlagsSupplier,
- IterationFinishedEvent iterationFinishedEvent) {
+ IterationFinishedEvent iterationFinishedEvent) {
this.clock = clock;
this.listeners = listeners;
- this.eventFlagsSupplier = eventFlagsSupplier;
this.iterationFinishedEvent = iterationFinishedEvent;
flushingEvents = new ArrayDeque<>();
queuedEvents = new ArrayDeque<>();
@@ -132,9 +120,8 @@ public final class ListenerSet {
* @return The copied listener set.
*/
@CheckResult
- public ListenerSet copy(
- Looper looper, IterationFinishedEvent iterationFinishedEvent) {
- return new ListenerSet<>(listeners, looper, clock, eventFlagsSupplier, iterationFinishedEvent);
+ public ListenerSet copy(Looper looper, IterationFinishedEvent iterationFinishedEvent) {
+ return new ListenerSet<>(listeners, looper, clock, iterationFinishedEvent);
}
/**
@@ -149,7 +136,7 @@ public final class ListenerSet {
return;
}
Assertions.checkNotNull(listener);
- listeners.add(new ListenerHolder<>(listener, eventFlagsSupplier));
+ listeners.add(new ListenerHolder<>(listener));
}
/**
@@ -160,7 +147,7 @@ public final class ListenerSet {
* @param listener The listener to be removed.
*/
public void remove(T listener) {
- for (ListenerHolder listenerHolder : listeners) {
+ for (ListenerHolder listenerHolder : listeners) {
if (listenerHolder.listener.equals(listener)) {
listenerHolder.release(iterationFinishedEvent);
listeners.remove(listenerHolder);
@@ -176,11 +163,10 @@ public final class ListenerSet {
* @param event The event.
*/
public void queueEvent(int eventFlag, Event event) {
- CopyOnWriteArraySet> listenerSnapshot =
- new CopyOnWriteArraySet<>(listeners);
+ CopyOnWriteArraySet> listenerSnapshot = new CopyOnWriteArraySet<>(listeners);
queuedEvents.add(
() -> {
- for (ListenerHolder holder : listenerSnapshot) {
+ for (ListenerHolder holder : listenerSnapshot) {
holder.invoke(eventFlag, event);
}
});
@@ -226,7 +212,7 @@ public final class ListenerSet {
*
This will ensure no events are sent to any listener after this method has been called.
*/
public void release() {
- for (ListenerHolder listenerHolder : listeners) {
+ for (ListenerHolder listenerHolder : listeners) {
listenerHolder.release(iterationFinishedEvent);
}
listeners.clear();
@@ -249,8 +235,8 @@ public final class ListenerSet {
private boolean handleMessage(Message message) {
if (message.what == MSG_ITERATION_FINISHED) {
- for (ListenerHolder holder : listeners) {
- holder.iterationFinished(eventFlagsSupplier, iterationFinishedEvent);
+ for (ListenerHolder holder : listeners) {
+ holder.iterationFinished(iterationFinishedEvent);
if (handler.hasMessages(MSG_ITERATION_FINISHED)) {
// The invocation above triggered new events (and thus scheduled a new message). We need
// to stop here because this new message will take care of informing every listener about
@@ -268,45 +254,44 @@ public final class ListenerSet {
return true;
}
- private static final class ListenerHolder {
+ private static final class ListenerHolder {
@Nonnull public final T listener;
- private E eventsFlags;
+ private ExoFlags.Builder flagsBuilder;
private boolean needsIterationFinishedEvent;
private boolean released;
- public ListenerHolder(@Nonnull T listener, Supplier eventFlagSupplier) {
+ public ListenerHolder(@Nonnull T listener) {
this.listener = listener;
- this.eventsFlags = eventFlagSupplier.get();
+ this.flagsBuilder = new ExoFlags.Builder();
}
- public void release(IterationFinishedEvent event) {
+ public void release(IterationFinishedEvent event) {
released = true;
if (needsIterationFinishedEvent) {
- event.invoke(listener, eventsFlags);
+ event.invoke(listener, flagsBuilder.build());
}
}
public void invoke(int eventFlag, Event event) {
if (!released) {
if (eventFlag != C.INDEX_UNSET) {
- eventsFlags.add(eventFlag);
+ flagsBuilder.add(eventFlag);
}
needsIterationFinishedEvent = true;
event.invoke(listener);
}
}
- public void iterationFinished(
- Supplier eventFlagSupplier, IterationFinishedEvent event) {
+ public void iterationFinished(IterationFinishedEvent