Merge branch 'dev-v2' into dev-v2
This commit is contained in:
commit
fd4998bcca
9
.gitignore
vendored
9
.gitignore
vendored
@ -37,6 +37,12 @@ local.properties
|
|||||||
proguard.cfg
|
proguard.cfg
|
||||||
proguard-project.txt
|
proguard-project.txt
|
||||||
|
|
||||||
|
# Bazel
|
||||||
|
bazel-bin
|
||||||
|
bazel-genfiles
|
||||||
|
bazel-out
|
||||||
|
bazel-testlogs
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
.DS_Store
|
.DS_Store
|
||||||
cmake-build-debug
|
cmake-build-debug
|
||||||
@ -66,3 +72,6 @@ extensions/cronet/jniLibs/*
|
|||||||
extensions/cronet/libs/*
|
extensions/cronet/libs/*
|
||||||
!extensions/cronet/libs/README.md
|
!extensions/cronet/libs/README.md
|
||||||
|
|
||||||
|
# Cast receiver
|
||||||
|
cast_receiver_app/external-js
|
||||||
|
cast_receiver_app/bazel-cast_receiver_app
|
||||||
|
10
.hgignore
10
.hgignore
@ -44,6 +44,12 @@ local.properties
|
|||||||
proguard.cfg
|
proguard.cfg
|
||||||
proguard-project.txt
|
proguard-project.txt
|
||||||
|
|
||||||
|
# Bazel
|
||||||
|
bazel-bin
|
||||||
|
bazel-genfiles
|
||||||
|
bazel-out
|
||||||
|
bazel-testlogs
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
.DS_Store
|
.DS_Store
|
||||||
cmake-build-debug
|
cmake-build-debug
|
||||||
@ -69,3 +75,7 @@ extensions/cronet/jniLibs/*
|
|||||||
!extensions/cronet/jniLibs/README.md
|
!extensions/cronet/jniLibs/README.md
|
||||||
extensions/cronet/libs/*
|
extensions/cronet/libs/*
|
||||||
!extensions/cronet/libs/README.md
|
!extensions/cronet/libs/README.md
|
||||||
|
|
||||||
|
# Cast receiver
|
||||||
|
cast_receiver_app/external-js
|
||||||
|
cast_receiver_app/bazel-cast_receiver_app
|
||||||
|
40
README.md
40
README.md
@ -27,6 +27,8 @@ repository and depend on the modules locally.
|
|||||||
|
|
||||||
### From JCenter ###
|
### From JCenter ###
|
||||||
|
|
||||||
|
#### 1. Add repositories ####
|
||||||
|
|
||||||
The easiest way to get started using ExoPlayer is to add it as a gradle
|
The easiest way to get started using ExoPlayer is to add it as a gradle
|
||||||
dependency. You need to make sure you have the Google and JCenter repositories
|
dependency. You need to make sure you have the Google and JCenter repositories
|
||||||
included in the `build.gradle` file in the root of your project:
|
included in the `build.gradle` file in the root of your project:
|
||||||
@ -38,6 +40,8 @@ repositories {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 2. Add ExoPlayer module dependencies ####
|
||||||
|
|
||||||
Next add a dependency in the `build.gradle` file of your app module. The
|
Next add a dependency in the `build.gradle` file of your app module. The
|
||||||
following will add a dependency to the full library:
|
following will add a dependency to the full library:
|
||||||
|
|
||||||
@ -45,15 +49,7 @@ following will add a dependency to the full library:
|
|||||||
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
||||||
```
|
```
|
||||||
|
|
||||||
where `2.X.X` is your preferred version. If not enabled already, you also need
|
where `2.X.X` is your preferred version.
|
||||||
to turn on Java 8 support in all `build.gradle` files depending on ExoPlayer, by
|
|
||||||
adding the following to the `android` section:
|
|
||||||
|
|
||||||
```gradle
|
|
||||||
compileOptions {
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
As an alternative to the full library, you can depend on only the library
|
As an alternative to the full library, you can depend on only the library
|
||||||
modules that you actually need. For example the following will add dependencies
|
modules that you actually need. For example the following will add dependencies
|
||||||
@ -87,6 +83,32 @@ JCenter can be found on [Bintray][].
|
|||||||
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
|
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
|
||||||
[Bintray]: https://bintray.com/google/exoplayer
|
[Bintray]: https://bintray.com/google/exoplayer
|
||||||
|
|
||||||
|
#### 3. Turn on Java 8 support ####
|
||||||
|
|
||||||
|
If not enabled already, you also need to turn on Java 8 support in all
|
||||||
|
`build.gradle` files depending on ExoPlayer, by adding the following to the
|
||||||
|
`android` section:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
compileOptions {
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that if you want to use Java 8 features in your own code, the following
|
||||||
|
additional options need to be set:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
// For Java compilers:
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
// For Kotlin compilers:
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Locally ###
|
### Locally ###
|
||||||
|
|
||||||
Cloning the repository and depending on the modules locally is required when
|
Cloning the repository and depending on the modules locally is required when
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
### dev-v2 (not yet released) ###
|
### dev-v2 (not yet released) ###
|
||||||
|
|
||||||
|
* `ExtractorMediaSource` renamed to `ProgressiveMediaSource`.
|
||||||
* Support for playing spherical videos on Daydream.
|
* Support for playing spherical videos on Daydream.
|
||||||
* Improve decoder re-use between playbacks. TODO: Write and link a blog post
|
* Improve decoder re-use between playbacks. TODO: Write and link a blog post
|
||||||
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)).
|
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)).
|
||||||
@ -17,7 +18,8 @@
|
|||||||
* Add `setStreamKeys` method to factories of DASH, SmoothStreaming and HLS
|
* Add `setStreamKeys` method to factories of DASH, SmoothStreaming and HLS
|
||||||
media sources to simplify filtering by downloaded streams.
|
media sources to simplify filtering by downloaded streams.
|
||||||
* Caching:
|
* Caching:
|
||||||
* Improve performance of `SimpleCache`.
|
* Improve performance of `SimpleCache`
|
||||||
|
([#4253](https://github.com/google/ExoPlayer/issues/4253)).
|
||||||
* Cache data with unknown length by default. The previous flag to opt in to
|
* Cache data with unknown length by default. The previous flag to opt in to
|
||||||
this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
|
this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
|
||||||
replaced with an opt out flag
|
replaced with an opt out flag
|
||||||
@ -27,20 +29,70 @@
|
|||||||
* Rename TaskState to DownloadState.
|
* Rename TaskState to DownloadState.
|
||||||
* Add new states to DownloadState.
|
* Add new states to DownloadState.
|
||||||
* Replace DownloadState.action with DownloadAction fields.
|
* Replace DownloadState.action with DownloadAction fields.
|
||||||
|
* DRM: Fix black flicker when keys rotate in DRM protected content
|
||||||
|
([#3561](https://github.com/google/ExoPlayer/issues/3561)).
|
||||||
* Add support for SHOUTcast ICY metadata
|
* Add support for SHOUTcast ICY metadata
|
||||||
([#3735](https://github.com/google/ExoPlayer/issues/3735)).
|
([#3735](https://github.com/google/ExoPlayer/issues/3735)).
|
||||||
* IMA extension:
|
* CEA-608: Improved conformance to the specification
|
||||||
* Clear ads loader listeners on release
|
([#3860](https://github.com/google/ExoPlayer/issues/3860)).
|
||||||
|
* IMA extension: Require setting the `Player` on `AdsLoader` instances before
|
||||||
|
playback.
|
||||||
|
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.
|
||||||
|
* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to
|
||||||
|
surface YUV output as the default. Remove constructor parameters `scaleToFit`
|
||||||
|
and `useSurfaceYuvOutput`.
|
||||||
|
* Change signature of `PlayerNotificationManager.NotificationListener` to better
|
||||||
|
fit service requirements. Remove ability to set a custom stop action.
|
||||||
|
* Fix issues with flickering notifications on KitKat.
|
||||||
|
`PlayerNotificationManager` has been fixed. Apps using
|
||||||
|
`DownloadNotificationUtil` should switch to using
|
||||||
|
`DownloadNotificationHelper`.
|
||||||
|
|
||||||
|
### 2.9.5 ###
|
||||||
|
|
||||||
|
* HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag.
|
||||||
|
* ConcatenatingMediaSource:
|
||||||
|
* Add `Handler` parameter to methods that take a callback `Runnable`.
|
||||||
|
* Fix issue with dropped messages when releasing the source
|
||||||
|
([#5464](https://github.com/google/ExoPlayer/issues/5464)).
|
||||||
|
* ExtractorMediaSource: Fix issue that could cause the player to get stuck
|
||||||
|
buffering at the end of the media.
|
||||||
|
* PlayerView: Fix issue preventing `OnClickListener` from receiving events
|
||||||
|
([#5433](https://github.com/google/ExoPlayer/issues/5433)).
|
||||||
|
* IMA extension: Upgrade IMA dependency to 3.10.6.
|
||||||
|
* Cronet extension: Upgrade Cronet dependency to 71.3578.98.
|
||||||
|
* OkHttp extension: Upgrade OkHttp dependency to 3.12.1.
|
||||||
|
* MP3: Wider fix for issue where streams would play twice on some Samsung
|
||||||
|
devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)).
|
||||||
|
|
||||||
|
### 2.9.4 ###
|
||||||
|
|
||||||
|
* IMA extension: Clear ads loader listeners on release
|
||||||
([#4114](https://github.com/google/ExoPlayer/issues/4114)).
|
([#4114](https://github.com/google/ExoPlayer/issues/4114)).
|
||||||
* Require setting the `Player` on `AdsLoader` instances before playback.
|
* SmoothStreaming: Fix support for subtitles in DRM protected streams
|
||||||
|
([#5378](https://github.com/google/ExoPlayer/issues/5378)).
|
||||||
* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior
|
* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior
|
||||||
of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)).
|
of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)).
|
||||||
|
* GVR extension: upgrade GVR SDK dependency to 1.190.0.
|
||||||
|
* Associate fatal player errors of type SOURCE with the loading source in
|
||||||
|
`AnalyticsListener.EventTime`
|
||||||
|
([#5407](https://github.com/google/ExoPlayer/issues/5407)).
|
||||||
|
* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where
|
||||||
|
using lazy preparation in `ConcatenatingMediaSource` with an
|
||||||
|
`ExtractorMediaSource` overrides initial seek positions
|
||||||
|
([#5350](https://github.com/google/ExoPlayer/issues/5350)).
|
||||||
|
* Add subtext to the `MediaDescriptionAdapter` of the
|
||||||
|
`PlayerNotificationManager`.
|
||||||
|
* Add workaround for video quality problems with Amlogic decoders
|
||||||
|
([#5003](https://github.com/google/ExoPlayer/issues/5003)).
|
||||||
* Fix issue where sending callbacks for playlist changes may cause problems
|
* Fix issue where sending callbacks for playlist changes may cause problems
|
||||||
because of parallel player access
|
because of parallel player access
|
||||||
([#5240](https://github.com/google/ExoPlayer/issues/5240)).
|
([#5240](https://github.com/google/ExoPlayer/issues/5240)).
|
||||||
* Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a
|
* Fix issue with reusing a `ClippingMediaSource` with an inner
|
||||||
callback `Runnable`.
|
`ExtractorMediaSource` and a non-zero start position
|
||||||
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.
|
([#5351](https://github.com/google/ExoPlayer/issues/5351)).
|
||||||
|
* Fix issue where uneven track durations in MP4 streams can cause OOM problems
|
||||||
|
([#3670](https://github.com/google/ExoPlayer/issues/3670)).
|
||||||
|
|
||||||
### 2.9.3 ###
|
### 2.9.3 ###
|
||||||
|
|
||||||
@ -1173,7 +1225,7 @@
|
|||||||
[here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi).
|
[here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi).
|
||||||
* Robustness improvements when handling MediaSource timeline changes and
|
* Robustness improvements when handling MediaSource timeline changes and
|
||||||
MediaPeriod transitions.
|
MediaPeriod transitions.
|
||||||
* EIA608: Support for caption styling and positioning.
|
* CEA-608: Support for caption styling and positioning.
|
||||||
* MPEG-TS: Improved support:
|
* MPEG-TS: Improved support:
|
||||||
* Support injection of custom TS payload readers.
|
* Support injection of custom TS payload readers.
|
||||||
* Support injection of custom section payload readers.
|
* Support injection of custom section payload readers.
|
||||||
@ -1417,8 +1469,8 @@ V2 release.
|
|||||||
(#801).
|
(#801).
|
||||||
* MP3: Fix playback of some streams when stream length is unknown.
|
* MP3: Fix playback of some streams when stream length is unknown.
|
||||||
* ID3: Support multiple frames of the same type in a single tag.
|
* ID3: Support multiple frames of the same type in a single tag.
|
||||||
* EIA608: Correctly handle repeated control characters, fixing an issue in which
|
* CEA-608: Correctly handle repeated control characters, fixing an issue in
|
||||||
captions would immediately disappear.
|
which captions would immediately disappear.
|
||||||
* AVC3: Fix decoder failures on some MediaTek devices in the case where the
|
* AVC3: Fix decoder failures on some MediaTek devices in the case where the
|
||||||
first buffer fed to the decoder does not start with SPS/PPS NAL units.
|
first buffer fed to the decoder does not start with SPS/PPS NAL units.
|
||||||
* Misc bug fixes.
|
* Misc bug fixes.
|
||||||
|
@ -13,13 +13,9 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.9.3'
|
releaseVersion = '2.9.5'
|
||||||
releaseVersionCode = 2009003
|
releaseVersionCode = 2009005
|
||||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
minSdkVersion = 16
|
||||||
// components provided by the library may be of use on older devices.
|
|
||||||
// However, please note that the core media playback functionality provided
|
|
||||||
// by the library requires API level 16 or greater.
|
|
||||||
minSdkVersion = 14
|
|
||||||
targetSdkVersion = 28
|
targetSdkVersion = 28
|
||||||
compileSdkVersion = 28
|
compileSdkVersion = 28
|
||||||
buildToolsVersion = '28.0.2'
|
buildToolsVersion = '28.0.2'
|
||||||
|
@ -26,7 +26,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +35,8 @@ import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
|||||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
@ -63,7 +63,7 @@ import java.util.ArrayList;
|
|||||||
private final SimpleExoPlayer exoPlayer;
|
private final SimpleExoPlayer exoPlayer;
|
||||||
private final CastPlayer castPlayer;
|
private final CastPlayer castPlayer;
|
||||||
private final ArrayList<MediaItem> mediaQueue;
|
private final ArrayList<MediaItem> mediaQueue;
|
||||||
private final QueuePositionListener queuePositionListener;
|
private final QueueChangesListener queueChangesListener;
|
||||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||||
|
|
||||||
private boolean castMediaQueueCreationPending;
|
private boolean castMediaQueueCreationPending;
|
||||||
@ -71,32 +71,21 @@ import java.util.ArrayList;
|
|||||||
private Player currentPlayer;
|
private Player currentPlayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param queuePositionListener A {@link QueuePositionListener} for queue position changes.
|
* Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
|
||||||
|
*
|
||||||
|
* @param queueChangesListener A {@link QueueChangesListener} for queue position changes.
|
||||||
* @param localPlayerView The {@link PlayerView} for local playback.
|
* @param localPlayerView The {@link PlayerView} for local playback.
|
||||||
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
||||||
* @param context A {@link Context}.
|
* @param context A {@link Context}.
|
||||||
* @param castContext The {@link CastContext}.
|
* @param castContext The {@link CastContext}.
|
||||||
*/
|
*/
|
||||||
public static DefaultReceiverPlayerManager createPlayerManager(
|
public DefaultReceiverPlayerManager(
|
||||||
QueuePositionListener queuePositionListener,
|
QueueChangesListener queueChangesListener,
|
||||||
PlayerView localPlayerView,
|
PlayerView localPlayerView,
|
||||||
PlayerControlView castControlView,
|
PlayerControlView castControlView,
|
||||||
Context context,
|
Context context,
|
||||||
CastContext castContext) {
|
CastContext castContext) {
|
||||||
DefaultReceiverPlayerManager defaultReceiverPlayerManager =
|
this.queueChangesListener = queueChangesListener;
|
||||||
new DefaultReceiverPlayerManager(
|
|
||||||
queuePositionListener, localPlayerView, castControlView, context, castContext);
|
|
||||||
defaultReceiverPlayerManager.init();
|
|
||||||
return defaultReceiverPlayerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DefaultReceiverPlayerManager(
|
|
||||||
QueuePositionListener queuePositionListener,
|
|
||||||
PlayerView localPlayerView,
|
|
||||||
PlayerControlView castControlView,
|
|
||||||
Context context,
|
|
||||||
CastContext castContext) {
|
|
||||||
this.queuePositionListener = queuePositionListener;
|
|
||||||
this.localPlayerView = localPlayerView;
|
this.localPlayerView = localPlayerView;
|
||||||
this.castControlView = castControlView;
|
this.castControlView = castControlView;
|
||||||
mediaQueue = new ArrayList<>();
|
mediaQueue = new ArrayList<>();
|
||||||
@ -113,6 +102,8 @@ import java.util.ArrayList;
|
|||||||
castPlayer.addListener(this);
|
castPlayer.addListener(this);
|
||||||
castPlayer.setSessionAvailabilityListener(this);
|
castPlayer.setSessionAvailabilityListener(this);
|
||||||
castControlView.setPlayer(castPlayer);
|
castControlView.setPlayer(castPlayer);
|
||||||
|
|
||||||
|
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue manipulation methods.
|
// Queue manipulation methods.
|
||||||
@ -287,10 +278,6 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private void init() {
|
|
||||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCurrentItemIndex() {
|
private void updateCurrentItemIndex() {
|
||||||
int playbackState = currentPlayer.getPlaybackState();
|
int playbackState = currentPlayer.getPlaybackState();
|
||||||
maybeSetCurrentItemAndNotify(
|
maybeSetCurrentItemAndNotify(
|
||||||
@ -372,7 +359,7 @@ import java.util.ArrayList;
|
|||||||
if (this.currentItemIndex != currentItemIndex) {
|
if (this.currentItemIndex != currentItemIndex) {
|
||||||
int oldIndex = this.currentItemIndex;
|
int oldIndex = this.currentItemIndex;
|
||||||
this.currentItemIndex = currentItemIndex;
|
this.currentItemIndex = currentItemIndex;
|
||||||
queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
queueChangesListener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,7 +373,7 @@ import java.util.ArrayList;
|
|||||||
case DemoUtil.MIME_TYPE_HLS:
|
case DemoUtil.MIME_TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||||
return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||||
default: {
|
default: {
|
||||||
throw new IllegalStateException("Unsupported type: " + item.mimeType);
|
throw new IllegalStateException("Unsupported type: " + item.mimeType);
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,14 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.castdemo;
|
package com.google.android.exoplayer2.castdemo;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/** Utility methods and constants for the Cast demo application. */
|
/** Utility methods and constants for the Cast demo application. */
|
||||||
/* package */ final class DemoUtil {
|
/* package */ final class DemoUtil {
|
||||||
@ -32,6 +36,16 @@ import java.util.List;
|
|||||||
public final String name;
|
public final String name;
|
||||||
/** The mime type of the sample media content. */
|
/** The mime type of the sample media content. */
|
||||||
public final String mimeType;
|
public final String mimeType;
|
||||||
|
/**
|
||||||
|
* The {@link UUID} of the DRM scheme that protects the content, or null if the content is not
|
||||||
|
* DRM-protected.
|
||||||
|
*/
|
||||||
|
@Nullable public final UUID drmSchemeUuid;
|
||||||
|
/**
|
||||||
|
* The url from which players should obtain DRM licenses, or null if the content is not
|
||||||
|
* DRM-protected.
|
||||||
|
*/
|
||||||
|
@Nullable public final Uri licenseServerUri;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param uri See {@link #uri}.
|
* @param uri See {@link #uri}.
|
||||||
@ -39,9 +53,21 @@ import java.util.List;
|
|||||||
* @param mimeType See {@link #mimeType}.
|
* @param mimeType See {@link #mimeType}.
|
||||||
*/
|
*/
|
||||||
public Sample(String uri, String name, String mimeType) {
|
public Sample(String uri, String name, String mimeType) {
|
||||||
|
this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Sample(
|
||||||
|
String uri,
|
||||||
|
String name,
|
||||||
|
String mimeType,
|
||||||
|
@Nullable UUID drmSchemeUuid,
|
||||||
|
@Nullable String licenseServerUriString) {
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
|
this.drmSchemeUuid = drmSchemeUuid;
|
||||||
|
this.licenseServerUri =
|
||||||
|
licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -62,25 +88,15 @@ import java.util.List;
|
|||||||
// App samples.
|
// App samples.
|
||||||
ArrayList<Sample> samples = new ArrayList<>();
|
ArrayList<Sample> samples = new ArrayList<>();
|
||||||
|
|
||||||
|
// Clear content.
|
||||||
samples.add(
|
samples.add(
|
||||||
new Sample(
|
new Sample(
|
||||||
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
||||||
"DASH (clear,MP4,H264)",
|
"Clear DASH: Tears",
|
||||||
MIME_TYPE_DASH));
|
MIME_TYPE_DASH));
|
||||||
samples.add(
|
samples.add(
|
||||||
new Sample(
|
new Sample(
|
||||||
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
|
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4));
|
||||||
+ "hls/TearsOfSteel.m3u8",
|
|
||||||
"Tears of Steel (HLS)",
|
|
||||||
MIME_TYPE_HLS));
|
|
||||||
samples.add(
|
|
||||||
new Sample(
|
|
||||||
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3"
|
|
||||||
+ "/bipbop_4x3_variant.m3u8",
|
|
||||||
"HLS Basic (TS)",
|
|
||||||
MIME_TYPE_HLS));
|
|
||||||
samples.add(
|
|
||||||
new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)", MIME_TYPE_VIDEO_MP4));
|
|
||||||
SAMPLES = Collections.unmodifiableList(samples);
|
SAMPLES = Collections.unmodifiableList(samples);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,13 +42,14 @@ import com.google.android.gms.cast.CastMediaControlIntent;
|
|||||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
import com.google.android.gms.cast.framework.CastContext;
|
||||||
import com.google.android.gms.dynamite.DynamiteModule;
|
import com.google.android.gms.dynamite.DynamiteModule;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
|
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
|
||||||
* Cast extension.
|
* Cast extension.
|
||||||
*/
|
*/
|
||||||
public class MainActivity extends AppCompatActivity
|
public class MainActivity extends AppCompatActivity
|
||||||
implements OnClickListener, PlayerManager.QueuePositionListener {
|
implements OnClickListener, PlayerManager.QueueChangesListener {
|
||||||
|
|
||||||
private final MediaItem.Builder mediaItemBuilder;
|
private final MediaItem.Builder mediaItemBuilder;
|
||||||
|
|
||||||
@ -120,8 +121,8 @@ public class MainActivity extends AppCompatActivity
|
|||||||
switch (applicationId) {
|
switch (applicationId) {
|
||||||
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
|
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
|
||||||
playerManager =
|
playerManager =
|
||||||
DefaultReceiverPlayerManager.createPlayerManager(
|
new DefaultReceiverPlayerManager(
|
||||||
/* queuePositionListener= */ this,
|
/* queueChangesListener= */ this,
|
||||||
localPlayerView,
|
localPlayerView,
|
||||||
castControlView,
|
castControlView,
|
||||||
/* context= */ this,
|
/* context= */ this,
|
||||||
@ -161,7 +162,7 @@ public class MainActivity extends AppCompatActivity
|
|||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayerManager.QueuePositionListener implementation.
|
// PlayerManager.QueueChangesListener implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onQueuePositionChanged(int previousIndex, int newIndex) {
|
public void onQueuePositionChanged(int previousIndex, int newIndex) {
|
||||||
@ -173,6 +174,11 @@ public class MainActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onQueueContentsExternallyChanged() {
|
||||||
|
mediaQueueListAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private View buildSampleListView() {
|
private View buildSampleListView() {
|
||||||
@ -182,13 +188,18 @@ public class MainActivity extends AppCompatActivity
|
|||||||
sampleList.setOnItemClickListener(
|
sampleList.setOnItemClickListener(
|
||||||
(parent, view, position, id) -> {
|
(parent, view, position, id) -> {
|
||||||
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position);
|
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position);
|
||||||
playerManager.addItem(
|
|
||||||
mediaItemBuilder
|
mediaItemBuilder
|
||||||
.clear()
|
.clear()
|
||||||
.setMedia(sample.uri)
|
.setMedia(sample.uri)
|
||||||
.setTitle(sample.name)
|
.setTitle(sample.name)
|
||||||
.setMimeType(sample.mimeType)
|
.setMimeType(sample.mimeType);
|
||||||
.build());
|
if (sample.drmSchemeUuid != null) {
|
||||||
|
mediaItemBuilder.setDrmSchemes(
|
||||||
|
Collections.singletonList(
|
||||||
|
new MediaItem.DrmScheme(
|
||||||
|
sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri))));
|
||||||
|
}
|
||||||
|
playerManager.addItem(mediaItemBuilder.build());
|
||||||
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
|
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
|
||||||
});
|
});
|
||||||
return dialogList;
|
return dialogList;
|
||||||
@ -268,6 +279,8 @@ public class MainActivity extends AppCompatActivity
|
|||||||
int position = viewHolder.getAdapterPosition();
|
int position = viewHolder.getAdapterPosition();
|
||||||
if (playerManager.removeItem(position)) {
|
if (playerManager.removeItem(position)) {
|
||||||
mediaQueueListAdapter.notifyItemRemoved(position);
|
mediaQueueListAdapter.notifyItemRemoved(position);
|
||||||
|
// Update whichever item took its place, in case it became the new selected item.
|
||||||
|
mediaQueueListAdapter.notifyItemChanged(position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,14 +22,14 @@ import com.google.android.exoplayer2.ext.cast.MediaItem;
|
|||||||
/** Manages the players in the Cast demo app. */
|
/** Manages the players in the Cast demo app. */
|
||||||
interface PlayerManager {
|
interface PlayerManager {
|
||||||
|
|
||||||
/** Listener for changes in the media queue playback position. */
|
/** Listener for changes in the media queue. */
|
||||||
interface QueuePositionListener {
|
interface QueueChangesListener {
|
||||||
|
|
||||||
/**
|
/** Called when the currently played item of the media queue changes. */
|
||||||
* Called when the currently played item of the media queue changes.
|
|
||||||
*/
|
|
||||||
void onQueuePositionChanged(int previousIndex, int newIndex);
|
void onQueuePositionChanged(int previousIndex, int newIndex);
|
||||||
|
|
||||||
|
/** Called when the media queue changes due to modifications not caused by this manager. */
|
||||||
|
void onQueueContentsExternallyChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Redirects the given {@code keyEvent} to the active player. */
|
/** Redirects the given {@code keyEvent} to the active player. */
|
||||||
|
@ -26,7 +26,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,16 +23,12 @@ import com.google.android.exoplayer2.ExoPlayer;
|
|||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
@ -56,14 +52,9 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void init(Context context, PlayerView playerView) {
|
public void init(Context context, PlayerView playerView) {
|
||||||
// Create a default track selector.
|
|
||||||
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
|
|
||||||
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
|
|
||||||
|
|
||||||
// Create a player instance.
|
// Create a player instance.
|
||||||
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
|
player = ExoPlayerFactory.newSimpleInstance(context);
|
||||||
|
adsLoader.setPlayer(player);
|
||||||
// Bind the player to the view.
|
|
||||||
playerView.setPlayer(player);
|
playerView.setPlayer(player);
|
||||||
|
|
||||||
// This is the MediaSource representing the content media (i.e. not the ad).
|
// This is the MediaSource representing the content media (i.e. not the ad).
|
||||||
@ -89,6 +80,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
contentPosition = player.getContentPosition();
|
contentPosition = player.getContentPosition();
|
||||||
player.release();
|
player.release();
|
||||||
player = null;
|
player = null;
|
||||||
|
adsLoader.setPlayer(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +117,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,11 @@ package com.google.android.exoplayer2.demo;
|
|||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
|
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||||
|
import com.google.android.exoplayer2.offline.ActionFile;
|
||||||
|
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
||||||
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
|
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadIndexUtil;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
@ -31,14 +35,17 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
|||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
||||||
*/
|
*/
|
||||||
public class DemoApplication extends Application {
|
public class DemoApplication extends Application {
|
||||||
|
|
||||||
|
private static final String TAG = "DemoApplication";
|
||||||
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
||||||
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||||
@ -97,19 +104,28 @@ public class DemoApplication extends Application {
|
|||||||
|
|
||||||
private synchronized void initDownloadManager() {
|
private synchronized void initDownloadManager() {
|
||||||
if (downloadManager == null) {
|
if (downloadManager == null) {
|
||||||
|
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(new ExoDatabaseProvider(this));
|
||||||
|
File actionFile = new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE);
|
||||||
|
if (actionFile.exists()) {
|
||||||
|
try {
|
||||||
|
DownloadIndexUtil.upgradeActionFile(new ActionFile(actionFile), downloadIndex, null);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Upgrading action file failed", e);
|
||||||
|
}
|
||||||
|
actionFile.delete();
|
||||||
|
}
|
||||||
DownloaderConstructorHelper downloaderConstructorHelper =
|
DownloaderConstructorHelper downloaderConstructorHelper =
|
||||||
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
||||||
downloadManager =
|
downloadManager =
|
||||||
new DownloadManager(
|
new DownloadManager(
|
||||||
|
this,
|
||||||
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
|
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
|
||||||
new DefaultDownloaderFactory(downloaderConstructorHelper),
|
new DefaultDownloaderFactory(downloaderConstructorHelper),
|
||||||
MAX_SIMULTANEOUS_DOWNLOADS,
|
MAX_SIMULTANEOUS_DOWNLOADS,
|
||||||
DownloadManager.DEFAULT_MIN_RETRY_COUNT);
|
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
|
||||||
|
DownloadManager.DEFAULT_REQUIREMENTS);
|
||||||
downloadTracker =
|
downloadTracker =
|
||||||
new DownloadTracker(
|
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadIndex);
|
||||||
/* context= */ this,
|
|
||||||
buildDataSourceFactory(),
|
|
||||||
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
|
|
||||||
downloadManager.addListener(downloadTracker);
|
downloadManager.addListener(downloadTracker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import com.google.android.exoplayer2.offline.DownloadManager;
|
|||||||
import com.google.android.exoplayer2.offline.DownloadService;
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
import com.google.android.exoplayer2.offline.DownloadState;
|
import com.google.android.exoplayer2.offline.DownloadState;
|
||||||
import com.google.android.exoplayer2.scheduler.PlatformScheduler;
|
import com.google.android.exoplayer2.scheduler.PlatformScheduler;
|
||||||
import com.google.android.exoplayer2.ui.DownloadNotificationUtil;
|
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
|
||||||
import com.google.android.exoplayer2.util.NotificationUtil;
|
import com.google.android.exoplayer2.util.NotificationUtil;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
@ -33,6 +33,8 @@ public class DemoDownloadService extends DownloadService {
|
|||||||
|
|
||||||
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||||
|
|
||||||
|
private DownloadNotificationHelper notificationHelper;
|
||||||
|
|
||||||
public DemoDownloadService() {
|
public DemoDownloadService() {
|
||||||
super(
|
super(
|
||||||
FOREGROUND_NOTIFICATION_ID,
|
FOREGROUND_NOTIFICATION_ID,
|
||||||
@ -42,6 +44,12 @@ public class DemoDownloadService extends DownloadService {
|
|||||||
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected DownloadManager getDownloadManager() {
|
protected DownloadManager getDownloadManager() {
|
||||||
return ((DemoApplication) getApplication()).getDownloadManager();
|
return ((DemoApplication) getApplication()).getDownloadManager();
|
||||||
@ -54,32 +62,23 @@ public class DemoDownloadService extends DownloadService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Notification getForegroundNotification(DownloadState[] downloadStates) {
|
protected Notification getForegroundNotification(DownloadState[] downloadStates) {
|
||||||
return DownloadNotificationUtil.buildProgressNotification(
|
return notificationHelper.buildProgressNotification(
|
||||||
/* context= */ this,
|
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloadStates);
|
||||||
R.drawable.ic_download,
|
|
||||||
CHANNEL_ID,
|
|
||||||
/* contentIntent= */ null,
|
|
||||||
/* message= */ null,
|
|
||||||
downloadStates);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDownloadStateChanged(DownloadState downloadState) {
|
protected void onDownloadStateChanged(DownloadState downloadState) {
|
||||||
Notification notification = null;
|
Notification notification;
|
||||||
if (downloadState.state == DownloadState.STATE_COMPLETED) {
|
if (downloadState.state == DownloadState.STATE_COMPLETED) {
|
||||||
notification =
|
notification =
|
||||||
DownloadNotificationUtil.buildDownloadCompletedNotification(
|
notificationHelper.buildDownloadCompletedNotification(
|
||||||
/* context= */ this,
|
|
||||||
R.drawable.ic_download_done,
|
R.drawable.ic_download_done,
|
||||||
CHANNEL_ID,
|
|
||||||
/* contentIntent= */ null,
|
/* contentIntent= */ null,
|
||||||
Util.fromUtf8Bytes(downloadState.customMetadata));
|
Util.fromUtf8Bytes(downloadState.customMetadata));
|
||||||
} else if (downloadState.state == DownloadState.STATE_FAILED) {
|
} else if (downloadState.state == DownloadState.STATE_FAILED) {
|
||||||
notification =
|
notification =
|
||||||
DownloadNotificationUtil.buildDownloadFailedNotification(
|
notificationHelper.buildDownloadFailedNotification(
|
||||||
/* context= */ this,
|
|
||||||
R.drawable.ic_download_done,
|
R.drawable.ic_download_done,
|
||||||
CHANNEL_ID,
|
|
||||||
/* contentIntent= */ null,
|
/* contentIntent= */ null,
|
||||||
Util.fromUtf8Bytes(downloadState.customMetadata));
|
Util.fromUtf8Bytes(downloadState.customMetadata));
|
||||||
} else {
|
} else {
|
||||||
|
@ -34,17 +34,16 @@ import android.widget.Toast;
|
|||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.offline.ActionFile;
|
import com.google.android.exoplayer2.offline.ActionFile;
|
||||||
|
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
||||||
import com.google.android.exoplayer2.offline.DownloadAction;
|
import com.google.android.exoplayer2.offline.DownloadAction;
|
||||||
import com.google.android.exoplayer2.offline.DownloadHelper;
|
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloadService;
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
import com.google.android.exoplayer2.offline.DownloadState;
|
import com.google.android.exoplayer2.offline.DownloadState;
|
||||||
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
|
import com.google.android.exoplayer2.offline.DownloadStateCursor;
|
||||||
import com.google.android.exoplayer2.offline.StreamKey;
|
import com.google.android.exoplayer2.offline.StreamKey;
|
||||||
|
import com.google.android.exoplayer2.scheduler.Requirements;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper;
|
|
||||||
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper;
|
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
@ -54,8 +53,8 @@ import com.google.android.exoplayer2.ui.TrackSelectionView;
|
|||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -83,20 +82,21 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
private final DataSource.Factory dataSourceFactory;
|
private final DataSource.Factory dataSourceFactory;
|
||||||
private final TrackNameProvider trackNameProvider;
|
private final TrackNameProvider trackNameProvider;
|
||||||
private final CopyOnWriteArraySet<Listener> listeners;
|
private final CopyOnWriteArraySet<Listener> listeners;
|
||||||
private final HashMap<Uri, DownloadAction> trackedDownloadStates;
|
private final HashMap<Uri, DownloadState> trackedDownloadStates;
|
||||||
private final ActionFile actionFile;
|
private final DefaultDownloadIndex downloadIndex;
|
||||||
private final Handler actionFileWriteHandler;
|
private final Handler actionFileIOHandler;
|
||||||
|
|
||||||
public DownloadTracker(Context context, DataSource.Factory dataSourceFactory, File actionFile) {
|
public DownloadTracker(
|
||||||
|
Context context, DataSource.Factory dataSourceFactory, DefaultDownloadIndex downloadIndex) {
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
this.dataSourceFactory = dataSourceFactory;
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
this.actionFile = new ActionFile(actionFile);
|
this.downloadIndex = downloadIndex;
|
||||||
trackNameProvider = new DefaultTrackNameProvider(context.getResources());
|
trackNameProvider = new DefaultTrackNameProvider(context.getResources());
|
||||||
listeners = new CopyOnWriteArraySet<>();
|
listeners = new CopyOnWriteArraySet<>();
|
||||||
trackedDownloadStates = new HashMap<>();
|
trackedDownloadStates = new HashMap<>();
|
||||||
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
|
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
|
||||||
actionFileWriteThread.start();
|
actionFileWriteThread.start();
|
||||||
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
|
actionFileIOHandler = new Handler(actionFileWriteThread.getLooper());
|
||||||
loadTrackedActions();
|
loadTrackedActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
if (!trackedDownloadStates.containsKey(uri)) {
|
if (!trackedDownloadStates.containsKey(uri)) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
return trackedDownloadStates.get(uri).getKeys();
|
return Arrays.asList(trackedDownloadStates.get(uri).streamKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleDownload(
|
public void toggleDownload(
|
||||||
@ -149,7 +149,7 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
|| downloadState.state == DownloadState.STATE_FAILED) {
|
|| downloadState.state == DownloadState.STATE_FAILED) {
|
||||||
// A download has been removed, or has failed. Stop tracking it.
|
// A download has been removed, or has failed. Stop tracking it.
|
||||||
if (trackedDownloadStates.remove(downloadState.uri) != null) {
|
if (trackedDownloadStates.remove(downloadState.uri) != null) {
|
||||||
handleTrackedDownloadStatesChanged();
|
handleTrackedDownloadStateChanged(downloadState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,30 +159,35 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequirementsStateChanged(
|
||||||
|
DownloadManager downloadManager,
|
||||||
|
Requirements requirements,
|
||||||
|
@Requirements.RequirementFlags int notMetRequirements) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods
|
// Internal methods
|
||||||
|
|
||||||
private void loadTrackedActions() {
|
private void loadTrackedActions() {
|
||||||
try {
|
DownloadStateCursor downloadStates = downloadIndex.getDownloadStates();
|
||||||
DownloadAction[] allActions = actionFile.load();
|
while (downloadStates.moveToNext()) {
|
||||||
for (DownloadAction action : allActions) {
|
DownloadState downloadState = downloadStates.getDownloadState();
|
||||||
trackedDownloadStates.put(action.uri, action);
|
trackedDownloadStates.put(downloadState.uri, downloadState);
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Failed to load tracked actions", e);
|
|
||||||
}
|
}
|
||||||
|
downloadStates.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleTrackedDownloadStatesChanged() {
|
private void handleTrackedDownloadStateChanged(DownloadState downloadState) {
|
||||||
for (Listener listener : listeners) {
|
for (Listener listener : listeners) {
|
||||||
listener.onDownloadsChanged();
|
listener.onDownloadsChanged();
|
||||||
}
|
}
|
||||||
final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]);
|
actionFileIOHandler.post(
|
||||||
actionFileWriteHandler.post(
|
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
if (downloadState.state == DownloadState.STATE_REMOVED) {
|
||||||
actionFile.store(actions);
|
downloadIndex.removeDownloadState(downloadState.id);
|
||||||
} catch (IOException e) {
|
} else {
|
||||||
Log.e(TAG, "Failed to store tracked actions", e);
|
downloadIndex.putDownloadState(downloadState);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -192,8 +197,9 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
// This content is already being downloaded. Do nothing.
|
// This content is already being downloaded. Do nothing.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
trackedDownloadStates.put(action.uri, action);
|
DownloadState downloadState = new DownloadState(action);
|
||||||
handleTrackedDownloadStatesChanged();
|
trackedDownloadStates.put(downloadState.uri, downloadState);
|
||||||
|
handleTrackedDownloadStateChanged(downloadState);
|
||||||
startServiceWithAction(action);
|
startServiceWithAction(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,18 +207,18 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
|
DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DownloadHelper<?> getDownloadHelper(
|
private DownloadHelper getDownloadHelper(
|
||||||
Uri uri, String extension, RenderersFactory renderersFactory) {
|
Uri uri, String extension, RenderersFactory renderersFactory) {
|
||||||
int type = Util.inferContentType(uri, extension);
|
int type = Util.inferContentType(uri, extension);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return new DashDownloadHelper(uri, dataSourceFactory, renderersFactory);
|
return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsDownloadHelper(uri, dataSourceFactory, renderersFactory);
|
return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsDownloadHelper(uri, dataSourceFactory, renderersFactory);
|
return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ProgressiveDownloadHelper(uri);
|
return DownloadHelper.forProgressive(uri);
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
@ -222,10 +228,11 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
private final class StartDownloadDialogHelper
|
private final class StartDownloadDialogHelper
|
||||||
implements DownloadHelper.Callback,
|
implements DownloadHelper.Callback,
|
||||||
DialogInterface.OnClickListener,
|
DialogInterface.OnClickListener,
|
||||||
|
DialogInterface.OnDismissListener,
|
||||||
View.OnClickListener,
|
View.OnClickListener,
|
||||||
TrackSelectionView.DialogCallback {
|
TrackSelectionView.DialogCallback {
|
||||||
|
|
||||||
private final DownloadHelper<?> downloadHelper;
|
private final DownloadHelper downloadHelper;
|
||||||
private final String name;
|
private final String name;
|
||||||
private final LayoutInflater dialogInflater;
|
private final LayoutInflater dialogInflater;
|
||||||
private final AlertDialog dialog;
|
private final AlertDialog dialog;
|
||||||
@ -235,20 +242,21 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
private DefaultTrackSelector.Parameters parameters;
|
private DefaultTrackSelector.Parameters parameters;
|
||||||
|
|
||||||
private StartDownloadDialogHelper(
|
private StartDownloadDialogHelper(
|
||||||
Activity activity, DownloadHelper<?> downloadHelper, String name) {
|
Activity activity, DownloadHelper downloadHelper, String name) {
|
||||||
this.downloadHelper = downloadHelper;
|
this.downloadHelper = downloadHelper;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
AlertDialog.Builder builder =
|
AlertDialog.Builder builder =
|
||||||
new AlertDialog.Builder(activity)
|
new AlertDialog.Builder(activity)
|
||||||
.setTitle(R.string.download_preparing)
|
.setTitle(R.string.download_preparing)
|
||||||
.setPositiveButton(android.R.string.ok, this)
|
.setPositiveButton(android.R.string.ok, /* listener= */ this)
|
||||||
.setNegativeButton(android.R.string.cancel, null);
|
.setNegativeButton(android.R.string.cancel, /* listener= */ null);
|
||||||
|
|
||||||
// Inflate with the builder's context to ensure the correct style is used.
|
// Inflate with the builder's context to ensure the correct style is used.
|
||||||
dialogInflater = LayoutInflater.from(builder.getContext());
|
dialogInflater = LayoutInflater.from(builder.getContext());
|
||||||
selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null);
|
selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null);
|
||||||
builder.setView(selectionList);
|
builder.setView(selectionList);
|
||||||
dialog = builder.create();
|
dialog = builder.create();
|
||||||
|
dialog.setOnDismissListener(/* listener= */ this);
|
||||||
dialog.show();
|
dialog.show();
|
||||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||||
|
|
||||||
@ -259,19 +267,17 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
// DownloadHelper.Callback implementation.
|
// DownloadHelper.Callback implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepared(DownloadHelper<?> helper) {
|
public void onPrepared(DownloadHelper helper) {
|
||||||
if (helper.getPeriodCount() < 1) {
|
if (helper.getPeriodCount() > 0) {
|
||||||
onPrepareError(downloadHelper, new IOException("Content is empty."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||||
updateSelectionList();
|
updateSelectionList();
|
||||||
|
}
|
||||||
dialog.setTitle(R.string.exo_download_description);
|
dialog.setTitle(R.string.exo_download_description);
|
||||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareError(DownloadHelper<?> helper, IOException e) {
|
public void onPrepareError(DownloadHelper helper, IOException e) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
|
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
|
||||||
.show();
|
.show();
|
||||||
@ -317,6 +323,13 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
startDownload(downloadAction);
|
startDownload(downloadAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialogInterface.OnDismissListener implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialog) {
|
||||||
|
downloadHelper.release();
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private void updateSelectionList() {
|
private void updateSelectionList() {
|
||||||
|
@ -51,8 +51,8 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep
|
|||||||
import com.google.android.exoplayer2.offline.StreamKey;
|
import com.google.android.exoplayer2.offline.StreamKey;
|
||||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
@ -483,7 +483,7 @@ public class PlayerActivity extends Activity
|
|||||||
.setStreamKeys(offlineStreamKeys)
|
.setStreamKeys(offlineStreamKeys)
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
default: {
|
default: {
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 14
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
consumerProguardFiles 'proguard-rules.txt'
|
consumerProguardFiles 'proguard-rules.txt'
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ android {
|
|||||||
buildToolsVersion project.ext.buildToolsVersion
|
buildToolsVersion project.ext.buildToolsVersion
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'org.chromium.net:cronet-embedded:66.3359.158'
|
api 'org.chromium.net:cronet-embedded:71.3578.98'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
testImplementation project(modulePrefix + 'library')
|
testImplementation project(modulePrefix + 'library')
|
||||||
|
@ -28,8 +28,8 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
|
|||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Renderer;
|
import com.google.android.exoplayer2.Renderer;
|
||||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@ -86,7 +86,7 @@ public class FlacPlaybackTest {
|
|||||||
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
|
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
|
||||||
player.addListener(this);
|
player.addListener(this);
|
||||||
MediaSource mediaSource =
|
MediaSource mediaSource =
|
||||||
new ExtractorMediaSource.Factory(
|
new ProgressiveMediaSource.Factory(
|
||||||
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
|
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
|
||||||
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
|
@ -33,9 +33,7 @@ dependencies {
|
|||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation project(modulePrefix + 'library-ui')
|
implementation project(modulePrefix + 'library-ui')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
implementation 'com.google.vr:sdk-audio:1.80.0'
|
api 'com.google.vr:sdk-base:1.190.0'
|
||||||
implementation 'com.google.vr:sdk-controller:1.80.0'
|
|
||||||
api 'com.google.vr:sdk-base:1.80.0'
|
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,13 +31,13 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.2'
|
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.6'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.google.android.gms:play-services-ads:17.1.1'
|
implementation 'com.google.android.gms:play-services-ads:17.1.2'
|
||||||
// These dependencies are necessary to force the supportLibraryVersion of
|
// These dependencies are necessary to force the supportLibraryVersion of
|
||||||
// com.android.support:support-v4 and com.android.support:customtabs to be
|
// com.android.support:support-v4 and com.android.support:customtabs to be
|
||||||
// used. Else older versions are used, for example via:
|
// used. Else older versions are used, for example via:
|
||||||
// com.google.android.gms:play-services-ads:17.1.1
|
// com.google.android.gms:play-services-ads:17.1.2
|
||||||
// |-- com.android.support:customtabs:26.1.0
|
// |-- com.android.support:customtabs:26.1.0
|
||||||
implementation 'com.android.support:support-v4:' + supportLibraryVersion
|
implementation 'com.android.support:support-v4:' + supportLibraryVersion
|
||||||
implementation 'com.android.support:customtabs:' + supportLibraryVersion
|
implementation 'com.android.support:customtabs:' + supportLibraryVersion
|
||||||
|
@ -466,11 +466,11 @@ public final class ImaAdsLoader
|
|||||||
}
|
}
|
||||||
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
|
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
|
||||||
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
|
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
|
||||||
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings);
|
|
||||||
period = new Timeline.Period();
|
period = new Timeline.Period();
|
||||||
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
|
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
|
||||||
adDisplayContainer = imaFactory.createAdDisplayContainer();
|
adDisplayContainer = imaFactory.createAdDisplayContainer();
|
||||||
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
|
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
|
||||||
|
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
|
||||||
adsLoader.addAdErrorListener(/* adErrorListener= */ this);
|
adsLoader.addAdErrorListener(/* adErrorListener= */ this);
|
||||||
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
|
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
|
||||||
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
||||||
@ -524,7 +524,6 @@ public final class ImaAdsLoader
|
|||||||
if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
|
if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
|
||||||
request.setVastLoadTimeout(vastLoadTimeoutMs);
|
request.setVastLoadTimeout(vastLoadTimeoutMs);
|
||||||
}
|
}
|
||||||
request.setAdDisplayContainer(adDisplayContainer);
|
|
||||||
request.setContentProgressProvider(this);
|
request.setContentProgressProvider(this);
|
||||||
request.setUserRequestContext(pendingAdRequestContext);
|
request.setUserRequestContext(pendingAdRequestContext);
|
||||||
adsLoader.requestAds(request);
|
adsLoader.requestAds(request);
|
||||||
@ -1374,9 +1373,9 @@ public final class ImaAdsLoader
|
|||||||
AdDisplayContainer createAdDisplayContainer();
|
AdDisplayContainer createAdDisplayContainer();
|
||||||
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */
|
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */
|
||||||
AdsRequest createAdsRequest();
|
AdsRequest createAdsRequest();
|
||||||
/** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings) */
|
/** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */
|
||||||
com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
|
com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
|
||||||
Context context, ImaSdkSettings imaSdkSettings);
|
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */
|
/** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */
|
||||||
@ -1403,8 +1402,9 @@ public final class ImaAdsLoader
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
|
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
|
||||||
Context context, ImaSdkSettings imaSdkSettings) {
|
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
|
||||||
return ImaSdkFactory.getInstance().createAdsLoader(context, imaSdkSettings);
|
return ImaSdkFactory.getInstance()
|
||||||
|
.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,8 +93,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||||
return adsMediaSource.createPeriod(id, allocator);
|
return adsMediaSource.createPeriod(id, allocator, startPositionUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.ima;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.AdsLoader;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
|
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdsRequest;
|
import com.google.ads.interactivemedia.v3.api.AdsRequest;
|
||||||
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
|
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
|
||||||
@ -64,8 +65,8 @@ final class SingletonImaFactory implements ImaAdsLoader.ImaFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
|
public AdsLoader createAdsLoader(
|
||||||
Context context, ImaSdkSettings imaSdkSettings) {
|
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
|
||||||
return adsLoader;
|
return adsLoader;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ dependencies {
|
|||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
api 'com.squareup.okhttp3:okhttp:3.11.0'
|
api 'com.squareup.okhttp3:okhttp:3.12.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -28,8 +28,8 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
|
|||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Renderer;
|
import com.google.android.exoplayer2.Renderer;
|
||||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@ -86,7 +86,7 @@ public class OpusPlaybackTest {
|
|||||||
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
|
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
|
||||||
player.addListener(this);
|
player.addListener(this);
|
||||||
MediaSource mediaSource =
|
MediaSource mediaSource =
|
||||||
new ExtractorMediaSource.Factory(
|
new ProgressiveMediaSource.Factory(
|
||||||
new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"))
|
new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"))
|
||||||
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
|
@ -24,7 +24,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 15
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,26 +34,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
|
|||||||
NDK_PATH="<path to Android NDK>"
|
NDK_PATH="<path to Android NDK>"
|
||||||
```
|
```
|
||||||
|
|
||||||
* Fetch libvpx and libyuv:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd "${VP9_EXT_PATH}/jni" && \
|
|
||||||
git clone https://chromium.googlesource.com/webm/libvpx libvpx && \
|
|
||||||
git clone https://chromium.googlesource.com/libyuv/libyuv libyuv
|
|
||||||
```
|
|
||||||
|
|
||||||
* Checkout the appropriate branches of libvpx and libyuv (the scripts and
|
|
||||||
makefiles bundled in this repo are known to work only at these versions of the
|
|
||||||
libraries - we will update this periodically as newer versions of
|
|
||||||
libvpx/libyuv are released):
|
|
||||||
|
|
||||||
```
|
|
||||||
cd "${VP9_EXT_PATH}/jni/libvpx" && \
|
|
||||||
git checkout tags/v1.7.0 -b v1.7.0 && \
|
|
||||||
cd "${VP9_EXT_PATH}/jni/libyuv" && \
|
|
||||||
git checkout 996a2bbd
|
|
||||||
```
|
|
||||||
|
|
||||||
* Run a script that generates necessary configuration files for libvpx:
|
* Run a script that generates necessary configuration files for libvpx:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -78,10 +58,6 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
|
|||||||
* Android config scripts should be re-generated by running
|
* Android config scripts should be re-generated by running
|
||||||
`generate_libvpx_android_configs.sh`
|
`generate_libvpx_android_configs.sh`
|
||||||
* Clean and re-build the project.
|
* Clean and re-build the project.
|
||||||
* If you want to use your own version of libvpx or libyuv, place it in
|
|
||||||
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
|
|
||||||
please note that `generate_libvpx_android_configs.sh` and the makefiles need
|
|
||||||
to be modified to work with arbitrary versions of libvpx and libyuv.
|
|
||||||
|
|
||||||
## Using the extension ##
|
## Using the extension ##
|
||||||
|
|
||||||
|
@ -29,8 +29,8 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
|
|||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Renderer;
|
import com.google.android.exoplayer2.Renderer;
|
||||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
@ -114,12 +114,12 @@ public class VpxPlaybackTest {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Looper.prepare();
|
Looper.prepare();
|
||||||
LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0);
|
LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0);
|
||||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||||
player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector);
|
player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector);
|
||||||
player.addListener(this);
|
player.addListener(this);
|
||||||
MediaSource mediaSource =
|
MediaSource mediaSource =
|
||||||
new ExtractorMediaSource.Factory(
|
new ProgressiveMediaSource.Factory(
|
||||||
new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"))
|
new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"))
|
||||||
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
|
@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.vp9;
|
package com.google.android.exoplayer2.ext.vp9;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
@ -109,7 +107,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
/** The default input buffer size. */
|
/** The default input buffer size. */
|
||||||
private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
|
private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
|
||||||
|
|
||||||
private final boolean scaleToFit;
|
|
||||||
private final boolean disableLoopFilter;
|
private final boolean disableLoopFilter;
|
||||||
private final long allowedJoiningTimeMs;
|
private final long allowedJoiningTimeMs;
|
||||||
private final int maxDroppedFramesToNotify;
|
private final int maxDroppedFramesToNotify;
|
||||||
@ -119,7 +116,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
private final TimedValueQueue<Format> formatQueue;
|
private final TimedValueQueue<Format> formatQueue;
|
||||||
private final DecoderInputBuffer flagsOnlyBuffer;
|
private final DecoderInputBuffer flagsOnlyBuffer;
|
||||||
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||||
private final boolean useSurfaceYuvOutput;
|
|
||||||
|
|
||||||
private Format format;
|
private Format format;
|
||||||
private Format pendingFormat;
|
private Format pendingFormat;
|
||||||
@ -127,13 +123,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
private VpxDecoder decoder;
|
private VpxDecoder decoder;
|
||||||
private VpxInputBuffer inputBuffer;
|
private VpxInputBuffer inputBuffer;
|
||||||
private VpxOutputBuffer outputBuffer;
|
private VpxOutputBuffer outputBuffer;
|
||||||
private DrmSession<ExoMediaCrypto> drmSession;
|
@Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
|
||||||
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
@Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;
|
||||||
|
|
||||||
private @ReinitializationState int decoderReinitializationState;
|
private @ReinitializationState int decoderReinitializationState;
|
||||||
private boolean decoderReceivedBuffers;
|
private boolean decoderReceivedBuffers;
|
||||||
|
|
||||||
private Bitmap bitmap;
|
|
||||||
private boolean renderedFirstFrame;
|
private boolean renderedFirstFrame;
|
||||||
private long initialPositionUs;
|
private long initialPositionUs;
|
||||||
private long joiningDeadlineMs;
|
private long joiningDeadlineMs;
|
||||||
@ -158,16 +153,14 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
protected DecoderCounters decoderCounters;
|
protected DecoderCounters decoderCounters;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param scaleToFit Whether video frames should be scaled to fit when rendering.
|
|
||||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||||
* can attempt to seamlessly join an ongoing playback.
|
* can attempt to seamlessly join an ongoing playback.
|
||||||
*/
|
*/
|
||||||
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs) {
|
public LibvpxVideoRenderer(long allowedJoiningTimeMs) {
|
||||||
this(scaleToFit, allowedJoiningTimeMs, null, null, 0);
|
this(allowedJoiningTimeMs, null, null, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param scaleToFit Whether video frames should be scaled to fit when rendering.
|
|
||||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||||
* can attempt to seamlessly join an ongoing playback.
|
* can attempt to seamlessly join an ongoing playback.
|
||||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||||
@ -176,23 +169,22 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
|
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
|
||||||
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
|
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
|
||||||
*/
|
*/
|
||||||
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
|
public LibvpxVideoRenderer(
|
||||||
Handler eventHandler, VideoRendererEventListener eventListener,
|
long allowedJoiningTimeMs,
|
||||||
|
Handler eventHandler,
|
||||||
|
VideoRendererEventListener eventListener,
|
||||||
int maxDroppedFramesToNotify) {
|
int maxDroppedFramesToNotify) {
|
||||||
this(
|
this(
|
||||||
scaleToFit,
|
|
||||||
allowedJoiningTimeMs,
|
allowedJoiningTimeMs,
|
||||||
eventHandler,
|
eventHandler,
|
||||||
eventListener,
|
eventListener,
|
||||||
maxDroppedFramesToNotify,
|
maxDroppedFramesToNotify,
|
||||||
/* drmSessionManager= */ null,
|
/* drmSessionManager= */ null,
|
||||||
/* playClearSamplesWithoutKeys= */ false,
|
/* playClearSamplesWithoutKeys= */ false,
|
||||||
/* disableLoopFilter= */ false,
|
/* disableLoopFilter= */ false);
|
||||||
/* useSurfaceYuvOutput= */ false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param scaleToFit Whether video frames should be scaled to fit when rendering.
|
|
||||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||||
* can attempt to seamlessly join an ongoing playback.
|
* can attempt to seamlessly join an ongoing playback.
|
||||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||||
@ -208,26 +200,21 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||||
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
|
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
|
||||||
* @param useSurfaceYuvOutput Directly output YUV to the Surface via ANativeWindow.
|
|
||||||
*/
|
*/
|
||||||
public LibvpxVideoRenderer(
|
public LibvpxVideoRenderer(
|
||||||
boolean scaleToFit,
|
|
||||||
long allowedJoiningTimeMs,
|
long allowedJoiningTimeMs,
|
||||||
Handler eventHandler,
|
Handler eventHandler,
|
||||||
VideoRendererEventListener eventListener,
|
VideoRendererEventListener eventListener,
|
||||||
int maxDroppedFramesToNotify,
|
int maxDroppedFramesToNotify,
|
||||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||||
boolean playClearSamplesWithoutKeys,
|
boolean playClearSamplesWithoutKeys,
|
||||||
boolean disableLoopFilter,
|
boolean disableLoopFilter) {
|
||||||
boolean useSurfaceYuvOutput) {
|
|
||||||
super(C.TRACK_TYPE_VIDEO);
|
super(C.TRACK_TYPE_VIDEO);
|
||||||
this.scaleToFit = scaleToFit;
|
|
||||||
this.disableLoopFilter = disableLoopFilter;
|
this.disableLoopFilter = disableLoopFilter;
|
||||||
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
|
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
|
||||||
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
|
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
|
||||||
this.drmSessionManager = drmSessionManager;
|
this.drmSessionManager = drmSessionManager;
|
||||||
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||||
this.useSurfaceYuvOutput = useSurfaceYuvOutput;
|
|
||||||
joiningDeadlineMs = C.TIME_UNSET;
|
joiningDeadlineMs = C.TIME_UNSET;
|
||||||
clearReportedVideoSize();
|
clearReportedVideoSize();
|
||||||
formatHolder = new FormatHolder();
|
formatHolder = new FormatHolder();
|
||||||
@ -364,26 +351,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
clearReportedVideoSize();
|
clearReportedVideoSize();
|
||||||
clearRenderedFirstFrame();
|
clearRenderedFirstFrame();
|
||||||
try {
|
try {
|
||||||
|
setSourceDrmSession(null);
|
||||||
releaseDecoder();
|
releaseDecoder();
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
|
||||||
if (drmSession != null) {
|
|
||||||
drmSessionManager.releaseSession(drmSession);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
|
|
||||||
drmSessionManager.releaseSession(pendingDrmSession);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
drmSession = null;
|
|
||||||
pendingDrmSession = null;
|
|
||||||
decoderCounters.ensureUpdated();
|
|
||||||
eventDispatcher.disabled(decoderCounters);
|
eventDispatcher.disabled(decoderCounters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
|
protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
|
||||||
@ -433,18 +406,35 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
/** Releases the decoder. */
|
/** Releases the decoder. */
|
||||||
@CallSuper
|
@CallSuper
|
||||||
protected void releaseDecoder() {
|
protected void releaseDecoder() {
|
||||||
if (decoder == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
decoder.release();
|
|
||||||
decoder = null;
|
|
||||||
decoderCounters.decoderReleaseCount++;
|
|
||||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||||
decoderReceivedBuffers = false;
|
decoderReceivedBuffers = false;
|
||||||
buffersInCodecCount = 0;
|
buffersInCodecCount = 0;
|
||||||
|
if (decoder != null) {
|
||||||
|
decoder.release();
|
||||||
|
decoder = null;
|
||||||
|
decoderCounters.decoderReleaseCount++;
|
||||||
|
}
|
||||||
|
setDecoderDrmSession(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
|
||||||
|
DrmSession<ExoMediaCrypto> previous = sourceDrmSession;
|
||||||
|
sourceDrmSession = session;
|
||||||
|
releaseDrmSessionIfUnused(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
|
||||||
|
DrmSession<ExoMediaCrypto> previous = decoderDrmSession;
|
||||||
|
decoderDrmSession = session;
|
||||||
|
releaseDrmSessionIfUnused(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseDrmSessionIfUnused(@Nullable DrmSession<ExoMediaCrypto> session) {
|
||||||
|
if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
|
||||||
|
drmSessionManager.releaseSession(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -467,16 +457,20 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
throw ExoPlaybackException.createForRenderer(
|
throw ExoPlaybackException.createForRenderer(
|
||||||
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
||||||
}
|
}
|
||||||
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
|
DrmSession<ExoMediaCrypto> session =
|
||||||
if (pendingDrmSession == drmSession) {
|
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
|
||||||
drmSessionManager.releaseSession(pendingDrmSession);
|
if (session == decoderDrmSession || session == sourceDrmSession) {
|
||||||
|
// We already had this session. The manager must be reference counting, so release it once
|
||||||
|
// to get the count attributed to this renderer back down to 1.
|
||||||
|
drmSessionManager.releaseSession(session);
|
||||||
}
|
}
|
||||||
|
setSourceDrmSession(session);
|
||||||
} else {
|
} else {
|
||||||
pendingDrmSession = null;
|
setSourceDrmSession(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingDrmSession != drmSession) {
|
if (sourceDrmSession != decoderDrmSession) {
|
||||||
if (decoderReceivedBuffers) {
|
if (decoderReceivedBuffers) {
|
||||||
// Signal end of stream and wait for any final output buffers before re-initialization.
|
// Signal end of stream and wait for any final output buffers before re-initialization.
|
||||||
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
|
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
|
||||||
@ -579,18 +573,14 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
*/
|
*/
|
||||||
protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException {
|
protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException {
|
||||||
int bufferMode = outputBuffer.mode;
|
int bufferMode = outputBuffer.mode;
|
||||||
boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
|
|
||||||
boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null;
|
boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null;
|
||||||
boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
|
boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
|
||||||
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
|
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
|
||||||
if (!renderRgb && !renderYuv && !renderSurface) {
|
if (!renderYuv && !renderSurface) {
|
||||||
dropOutputBuffer(outputBuffer);
|
dropOutputBuffer(outputBuffer);
|
||||||
} else {
|
} else {
|
||||||
maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
|
maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
|
||||||
if (renderRgb) {
|
if (renderYuv) {
|
||||||
renderRgbFrame(outputBuffer, scaleToFit);
|
|
||||||
outputBuffer.release();
|
|
||||||
} else if (renderYuv) {
|
|
||||||
outputBufferRenderer.setOutputBuffer(outputBuffer);
|
outputBufferRenderer.setOutputBuffer(outputBuffer);
|
||||||
// The renderer will release the buffer.
|
// The renderer will release the buffer.
|
||||||
} else { // renderSurface
|
} else { // renderSurface
|
||||||
@ -668,8 +658,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
this.surface = surface;
|
this.surface = surface;
|
||||||
this.outputBufferRenderer = outputBufferRenderer;
|
this.outputBufferRenderer = outputBufferRenderer;
|
||||||
if (surface != null) {
|
if (surface != null) {
|
||||||
outputMode =
|
outputMode = VpxDecoder.OUTPUT_MODE_SURFACE_YUV;
|
||||||
useSurfaceYuvOutput ? VpxDecoder.OUTPUT_MODE_SURFACE_YUV : VpxDecoder.OUTPUT_MODE_RGB;
|
|
||||||
} else {
|
} else {
|
||||||
outputMode =
|
outputMode =
|
||||||
outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE;
|
outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE;
|
||||||
@ -704,12 +693,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
drmSession = pendingDrmSession;
|
setDecoderDrmSession(sourceDrmSession);
|
||||||
|
|
||||||
ExoMediaCrypto mediaCrypto = null;
|
ExoMediaCrypto mediaCrypto = null;
|
||||||
if (drmSession != null) {
|
if (decoderDrmSession != null) {
|
||||||
mediaCrypto = drmSession.getMediaCrypto();
|
mediaCrypto = decoderDrmSession.getMediaCrypto();
|
||||||
if (mediaCrypto == null) {
|
if (mediaCrypto == null) {
|
||||||
DrmSessionException drmError = drmSession.getError();
|
DrmSessionException drmError = decoderDrmSession.getError();
|
||||||
if (drmError != null) {
|
if (drmError != null) {
|
||||||
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
|
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
|
||||||
// input format causes the session to be replaced before it's used.
|
// input format causes the session to be replaced before it's used.
|
||||||
@ -731,8 +721,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
NUM_OUTPUT_BUFFERS,
|
NUM_OUTPUT_BUFFERS,
|
||||||
initialInputBufferSize,
|
initialInputBufferSize,
|
||||||
mediaCrypto,
|
mediaCrypto,
|
||||||
disableLoopFilter,
|
disableLoopFilter);
|
||||||
useSurfaceYuvOutput);
|
|
||||||
decoder.setOutputMode(outputMode);
|
decoder.setOutputMode(outputMode);
|
||||||
TraceUtil.endSection();
|
TraceUtil.endSection();
|
||||||
long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
|
long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
|
||||||
@ -922,33 +911,16 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||||
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
@DrmSession.State int drmSessionState = decoderDrmSession.getState();
|
||||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
|
||||||
}
|
}
|
||||||
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) {
|
|
||||||
if (bitmap == null
|
|
||||||
|| bitmap.getWidth() != outputBuffer.width
|
|
||||||
|| bitmap.getHeight() != outputBuffer.height) {
|
|
||||||
bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565);
|
|
||||||
}
|
|
||||||
bitmap.copyPixelsFromBuffer(outputBuffer.data);
|
|
||||||
Canvas canvas = surface.lockCanvas(null);
|
|
||||||
if (scale) {
|
|
||||||
canvas.scale(
|
|
||||||
((float) canvas.getWidth()) / outputBuffer.width,
|
|
||||||
((float) canvas.getHeight()) / outputBuffer.height);
|
|
||||||
}
|
|
||||||
canvas.drawBitmap(bitmap, 0, 0, null);
|
|
||||||
surface.unlockCanvasAndPost(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setJoiningDeadlineMs() {
|
private void setJoiningDeadlineMs() {
|
||||||
joiningDeadlineMs = allowedJoiningTimeMs > 0
|
joiningDeadlineMs = allowedJoiningTimeMs > 0
|
||||||
? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;
|
? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;
|
||||||
|
@ -31,8 +31,7 @@ import java.nio.ByteBuffer;
|
|||||||
|
|
||||||
public static final int OUTPUT_MODE_NONE = -1;
|
public static final int OUTPUT_MODE_NONE = -1;
|
||||||
public static final int OUTPUT_MODE_YUV = 0;
|
public static final int OUTPUT_MODE_YUV = 0;
|
||||||
public static final int OUTPUT_MODE_RGB = 1;
|
public static final int OUTPUT_MODE_SURFACE_YUV = 1;
|
||||||
public static final int OUTPUT_MODE_SURFACE_YUV = 2;
|
|
||||||
|
|
||||||
private static final int NO_ERROR = 0;
|
private static final int NO_ERROR = 0;
|
||||||
private static final int DECODE_ERROR = 1;
|
private static final int DECODE_ERROR = 1;
|
||||||
@ -52,7 +51,6 @@ import java.nio.ByteBuffer;
|
|||||||
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
|
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
|
||||||
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
|
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
|
||||||
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
|
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
|
||||||
* @param enableSurfaceYuvOutputMode Whether OUTPUT_MODE_SURFACE_YUV is allowed.
|
|
||||||
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
|
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
|
||||||
*/
|
*/
|
||||||
public VpxDecoder(
|
public VpxDecoder(
|
||||||
@ -60,8 +58,7 @@ import java.nio.ByteBuffer;
|
|||||||
int numOutputBuffers,
|
int numOutputBuffers,
|
||||||
int initialInputBufferSize,
|
int initialInputBufferSize,
|
||||||
ExoMediaCrypto exoMediaCrypto,
|
ExoMediaCrypto exoMediaCrypto,
|
||||||
boolean disableLoopFilter,
|
boolean disableLoopFilter)
|
||||||
boolean enableSurfaceYuvOutputMode)
|
|
||||||
throws VpxDecoderException {
|
throws VpxDecoderException {
|
||||||
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
||||||
if (!VpxLibrary.isAvailable()) {
|
if (!VpxLibrary.isAvailable()) {
|
||||||
@ -71,7 +68,7 @@ import java.nio.ByteBuffer;
|
|||||||
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
|
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
|
||||||
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
|
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
|
||||||
}
|
}
|
||||||
vpxDecContext = vpxInit(disableLoopFilter, enableSurfaceYuvOutputMode);
|
vpxDecContext = vpxInit(disableLoopFilter);
|
||||||
if (vpxDecContext == 0) {
|
if (vpxDecContext == 0) {
|
||||||
throw new VpxDecoderException("Failed to initialize decoder");
|
throw new VpxDecoderException("Failed to initialize decoder");
|
||||||
}
|
}
|
||||||
@ -86,8 +83,8 @@ import java.nio.ByteBuffer;
|
|||||||
/**
|
/**
|
||||||
* Sets the output mode for frames rendered by the decoder.
|
* Sets the output mode for frames rendered by the decoder.
|
||||||
*
|
*
|
||||||
* @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE}, {@link #OUTPUT_MODE_RGB}
|
* @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE} and {@link
|
||||||
* and {@link #OUTPUT_MODE_YUV}.
|
* #OUTPUT_MODE_YUV}.
|
||||||
*/
|
*/
|
||||||
public void setOutputMode(int outputMode) {
|
public void setOutputMode(int outputMode) {
|
||||||
this.outputMode = outputMode;
|
this.outputMode = outputMode;
|
||||||
@ -168,7 +165,7 @@ import java.nio.ByteBuffer;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private native long vpxInit(boolean disableLoopFilter, boolean enableSurfaceYuvOutputMode);
|
private native long vpxInit(boolean disableLoopFilter);
|
||||||
|
|
||||||
private native long vpxClose(long context);
|
private native long vpxClose(long context);
|
||||||
private native long vpxDecode(long context, ByteBuffer encoded, int length);
|
private native long vpxDecode(long context, ByteBuffer encoded, int length);
|
||||||
|
@ -60,36 +60,19 @@ public final class VpxOutputBuffer extends OutputBuffer {
|
|||||||
* Initializes the buffer.
|
* Initializes the buffer.
|
||||||
*
|
*
|
||||||
* @param timeUs The presentation timestamp for the buffer, in microseconds.
|
* @param timeUs The presentation timestamp for the buffer, in microseconds.
|
||||||
* @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE},
|
* @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE} and {@link
|
||||||
* {@link VpxDecoder#OUTPUT_MODE_RGB} and {@link VpxDecoder#OUTPUT_MODE_YUV}.
|
* VpxDecoder#OUTPUT_MODE_YUV}.
|
||||||
*/
|
*/
|
||||||
public void init(long timeUs, int mode) {
|
public void init(long timeUs, int mode) {
|
||||||
this.timeUs = timeUs;
|
this.timeUs = timeUs;
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resizes the buffer based on the given dimensions. Called via JNI after decoding completes.
|
|
||||||
* @return Whether the buffer was resized successfully.
|
|
||||||
*/
|
|
||||||
public boolean initForRgbFrame(int width, int height) {
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
this.yuvPlanes = null;
|
|
||||||
if (!isSafeToMultiply(width, height) || !isSafeToMultiply(width * height, 2)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int minimumRgbSize = width * height * 2;
|
|
||||||
initData(minimumRgbSize);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resizes the buffer based on the given stride. Called via JNI after decoding completes.
|
* Resizes the buffer based on the given stride. Called via JNI after decoding completes.
|
||||||
|
*
|
||||||
* @return Whether the buffer was resized successfully.
|
* @return Whether the buffer was resized successfully.
|
||||||
*/
|
*/
|
||||||
public boolean initForYuvFrame(int width, int height, int yStride, int uvStride,
|
public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) {
|
||||||
int colorspace) {
|
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.colorspace = colorspace;
|
this.colorspace = colorspace;
|
||||||
|
@ -17,12 +17,6 @@
|
|||||||
WORKING_DIR := $(call my-dir)
|
WORKING_DIR := $(call my-dir)
|
||||||
include $(CLEAR_VARS)
|
include $(CLEAR_VARS)
|
||||||
LIBVPX_ROOT := $(WORKING_DIR)/libvpx
|
LIBVPX_ROOT := $(WORKING_DIR)/libvpx
|
||||||
LIBYUV_ROOT := $(WORKING_DIR)/libyuv
|
|
||||||
|
|
||||||
# build libyuv_static.a
|
|
||||||
LOCAL_PATH := $(WORKING_DIR)
|
|
||||||
LIBYUV_DISABLE_JPEG := "yes"
|
|
||||||
include $(LIBYUV_ROOT)/Android.mk
|
|
||||||
|
|
||||||
# build libvpx.so
|
# build libvpx.so
|
||||||
LOCAL_PATH := $(WORKING_DIR)
|
LOCAL_PATH := $(WORKING_DIR)
|
||||||
@ -37,7 +31,7 @@ LOCAL_CPP_EXTENSION := .cc
|
|||||||
LOCAL_SRC_FILES := vpx_jni.cc
|
LOCAL_SRC_FILES := vpx_jni.cc
|
||||||
LOCAL_LDLIBS := -llog -lz -lm -landroid
|
LOCAL_LDLIBS := -llog -lz -lm -landroid
|
||||||
LOCAL_SHARED_LIBRARIES := libvpx
|
LOCAL_SHARED_LIBRARIES := libvpx
|
||||||
LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures
|
LOCAL_STATIC_LIBRARIES := cpufeatures
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
|
|
||||||
$(call import-module,android/cpufeatures)
|
$(call import-module,android/cpufeatures)
|
||||||
|
@ -30,8 +30,6 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <new>
|
#include <new>
|
||||||
|
|
||||||
#include "libyuv.h" // NOLINT
|
|
||||||
|
|
||||||
#define VPX_CODEC_DISABLE_COMPAT 1
|
#define VPX_CODEC_DISABLE_COMPAT 1
|
||||||
#include "vpx/vpx_decoder.h"
|
#include "vpx/vpx_decoder.h"
|
||||||
#include "vpx/vp8dx.h"
|
#include "vpx/vp8dx.h"
|
||||||
@ -61,7 +59,6 @@
|
|||||||
(JNIEnv* env, jobject thiz, ##__VA_ARGS__)\
|
(JNIEnv* env, jobject thiz, ##__VA_ARGS__)\
|
||||||
|
|
||||||
// JNI references for VpxOutputBuffer class.
|
// JNI references for VpxOutputBuffer class.
|
||||||
static jmethodID initForRgbFrame;
|
|
||||||
static jmethodID initForYuvFrame;
|
static jmethodID initForYuvFrame;
|
||||||
static jfieldID dataField;
|
static jfieldID dataField;
|
||||||
static jfieldID outputModeField;
|
static jfieldID outputModeField;
|
||||||
@ -393,11 +390,7 @@ class JniBufferManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct JniCtx {
|
struct JniCtx {
|
||||||
JniCtx(bool enableBufferManager) {
|
JniCtx() { buffer_manager = new JniBufferManager(); }
|
||||||
if (enableBufferManager) {
|
|
||||||
buffer_manager = new JniBufferManager();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
~JniCtx() {
|
~JniCtx() {
|
||||||
if (native_window) {
|
if (native_window) {
|
||||||
@ -440,9 +433,8 @@ int vpx_release_frame_buffer(void* priv, vpx_codec_frame_buffer_t* fb) {
|
|||||||
return buffer_manager->release(*(int*)fb->priv);
|
return buffer_manager->release(*(int*)fb->priv);
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
|
DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
|
||||||
jboolean enableBufferManager) {
|
JniCtx* context = new JniCtx();
|
||||||
JniCtx* context = new JniCtx(enableBufferManager);
|
|
||||||
context->decoder = new vpx_codec_ctx_t();
|
context->decoder = new vpx_codec_ctx_t();
|
||||||
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
|
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
|
||||||
cfg.threads = android_getCpuCount();
|
cfg.threads = android_getCpuCount();
|
||||||
@ -469,7 +461,6 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
if (enableBufferManager) {
|
|
||||||
err = vpx_codec_set_frame_buffer_functions(
|
err = vpx_codec_set_frame_buffer_functions(
|
||||||
context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,
|
context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,
|
||||||
context->buffer_manager);
|
context->buffer_manager);
|
||||||
@ -477,15 +468,12 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
|
|||||||
LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.",
|
LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.",
|
||||||
err);
|
err);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Populate JNI References.
|
// Populate JNI References.
|
||||||
const jclass outputBufferClass = env->FindClass(
|
const jclass outputBufferClass = env->FindClass(
|
||||||
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
|
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
|
||||||
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
|
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
|
||||||
"(IIIII)Z");
|
"(IIIII)Z");
|
||||||
initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame",
|
|
||||||
"(II)Z");
|
|
||||||
dataField = env->GetFieldID(outputBufferClass, "data",
|
dataField = env->GetFieldID(outputBufferClass, "data",
|
||||||
"Ljava/nio/ByteBuffer;");
|
"Ljava/nio/ByteBuffer;");
|
||||||
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
|
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
|
||||||
@ -537,28 +525,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int kOutputModeYuv = 0;
|
const int kOutputModeYuv = 0;
|
||||||
const int kOutputModeRgb = 1;
|
const int kOutputModeSurfaceYuv = 1;
|
||||||
const int kOutputModeSurfaceYuv = 2;
|
|
||||||
|
|
||||||
int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
|
int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
|
||||||
if (outputMode == kOutputModeRgb) {
|
if (outputMode == kOutputModeYuv) {
|
||||||
// resize buffer if required.
|
|
||||||
jboolean initResult = env->CallBooleanMethod(jOutputBuffer, initForRgbFrame,
|
|
||||||
img->d_w, img->d_h);
|
|
||||||
if (env->ExceptionCheck() || !initResult) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get pointer to the data buffer.
|
|
||||||
const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField);
|
|
||||||
uint8_t* const dst =
|
|
||||||
reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(dataObject));
|
|
||||||
|
|
||||||
libyuv::I420ToRGB565(img->planes[VPX_PLANE_Y], img->stride[VPX_PLANE_Y],
|
|
||||||
img->planes[VPX_PLANE_U], img->stride[VPX_PLANE_U],
|
|
||||||
img->planes[VPX_PLANE_V], img->stride[VPX_PLANE_V],
|
|
||||||
dst, img->d_w * 2, img->d_w, img->d_h);
|
|
||||||
} else if (outputMode == kOutputModeYuv) {
|
|
||||||
const int kColorspaceUnknown = 0;
|
const int kColorspaceUnknown = 0;
|
||||||
const int kColorspaceBT601 = 1;
|
const int kColorspaceBT601 = 1;
|
||||||
const int kColorspaceBT709 = 2;
|
const int kColorspaceBT709 = 2;
|
||||||
@ -616,9 +586,6 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
|||||||
}
|
}
|
||||||
} else if (outputMode == kOutputModeSurfaceYuv &&
|
} else if (outputMode == kOutputModeSurfaceYuv &&
|
||||||
img->fmt != VPX_IMG_FMT_I42016) {
|
img->fmt != VPX_IMG_FMT_I42016) {
|
||||||
if (!context->buffer_manager) {
|
|
||||||
return -1; // enableBufferManager was not set in vpxInit.
|
|
||||||
}
|
|
||||||
int id = *(int*)img->fb_priv;
|
int id = *(int*)img->fb_priv;
|
||||||
context->buffer_manager->add_ref(id);
|
context->buffer_manager->add_ref(id);
|
||||||
JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id);
|
JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id);
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
# Constructors accessed via reflection in DefaultRenderersFactory
|
# Constructors accessed via reflection in DefaultRenderersFactory
|
||||||
-dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer
|
-dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer
|
||||||
-keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer {
|
-keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer {
|
||||||
<init>(boolean, long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
|
<init>(long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
|
||||||
}
|
}
|
||||||
-dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer
|
-dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer
|
||||||
-keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer {
|
-keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer {
|
||||||
@ -44,5 +44,22 @@
|
|||||||
<init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offlineDownloaderConstructorHelper);
|
<init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offlineDownloaderConstructorHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Constructors accessed via reflection in DownloadHelper
|
||||||
|
-dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory
|
||||||
|
-keepclassmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory {
|
||||||
|
<init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
|
||||||
|
DashMediaSource createMediaSource(android.net.Uri);
|
||||||
|
}
|
||||||
|
-dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory
|
||||||
|
-keepclassmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory {
|
||||||
|
<init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
|
||||||
|
HlsMediaSource createMediaSource(android.net.Uri);
|
||||||
|
}
|
||||||
|
-dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory
|
||||||
|
-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
|
||||||
|
<init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
|
||||||
|
SsMediaSource createMediaSource(android.net.Uri);
|
||||||
|
}
|
||||||
|
|
||||||
# Don't warn about checkerframework
|
# Don't warn about checkerframework
|
||||||
-dontwarn org.checkerframework.**
|
-dontwarn org.checkerframework.**
|
||||||
|
@ -37,7 +37,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||||||
private SampleStream stream;
|
private SampleStream stream;
|
||||||
private Format[] streamFormats;
|
private Format[] streamFormats;
|
||||||
private long streamOffsetUs;
|
private long streamOffsetUs;
|
||||||
private boolean readEndOfStream;
|
private long readingPositionUs;
|
||||||
private boolean streamIsFinal;
|
private boolean streamIsFinal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,7 +46,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||||||
*/
|
*/
|
||||||
public BaseRenderer(int trackType) {
|
public BaseRenderer(int trackType) {
|
||||||
this.trackType = trackType;
|
this.trackType = trackType;
|
||||||
readEndOfStream = true;
|
readingPositionUs = C.TIME_END_OF_SOURCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -98,7 +98,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
Assertions.checkState(!streamIsFinal);
|
Assertions.checkState(!streamIsFinal);
|
||||||
this.stream = stream;
|
this.stream = stream;
|
||||||
readEndOfStream = false;
|
readingPositionUs = offsetUs;
|
||||||
streamFormats = formats;
|
streamFormats = formats;
|
||||||
streamOffsetUs = offsetUs;
|
streamOffsetUs = offsetUs;
|
||||||
onStreamChanged(formats, offsetUs);
|
onStreamChanged(formats, offsetUs);
|
||||||
@ -111,7 +111,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final boolean hasReadStreamToEnd() {
|
public final boolean hasReadStreamToEnd() {
|
||||||
return readEndOfStream;
|
return readingPositionUs == C.TIME_END_OF_SOURCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final long getReadingPositionUs() {
|
||||||
|
return readingPositionUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -132,7 +137,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||||||
@Override
|
@Override
|
||||||
public final void resetPosition(long positionUs) throws ExoPlaybackException {
|
public final void resetPosition(long positionUs) throws ExoPlaybackException {
|
||||||
streamIsFinal = false;
|
streamIsFinal = false;
|
||||||
readEndOfStream = false;
|
readingPositionUs = positionUs;
|
||||||
onPositionReset(positionUs, false);
|
onPositionReset(positionUs, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,10 +308,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||||||
int result = stream.readData(formatHolder, buffer, formatRequired);
|
int result = stream.readData(formatHolder, buffer, formatRequired);
|
||||||
if (result == C.RESULT_BUFFER_READ) {
|
if (result == C.RESULT_BUFFER_READ) {
|
||||||
if (buffer.isEndOfStream()) {
|
if (buffer.isEndOfStream()) {
|
||||||
readEndOfStream = true;
|
readingPositionUs = C.TIME_END_OF_SOURCE;
|
||||||
return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;
|
return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;
|
||||||
}
|
}
|
||||||
buffer.timeUs += streamOffsetUs;
|
buffer.timeUs += streamOffsetUs;
|
||||||
|
readingPositionUs = Math.max(readingPositionUs, buffer.timeUs);
|
||||||
} else if (result == C.RESULT_FORMAT_READ) {
|
} else if (result == C.RESULT_FORMAT_READ) {
|
||||||
Format format = formatHolder.format;
|
Format format = formatHolder.format;
|
||||||
if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
|
if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
|
||||||
@ -332,7 +338,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||||||
* Returns whether the upstream source is ready.
|
* Returns whether the upstream source is ready.
|
||||||
*/
|
*/
|
||||||
protected final boolean isSourceReady() {
|
protected final boolean isSourceReady() {
|
||||||
return readEndOfStream ? streamIsFinal : stream.isReady();
|
return hasReadStreamToEnd() ? streamIsFinal : stream.isReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -460,8 +460,8 @@ public final class C {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
|
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
|
||||||
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_ENCRYPTED} and
|
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE},
|
||||||
* {@link #BUFFER_FLAG_DECODE_ONLY}.
|
* {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}.
|
||||||
*/
|
*/
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@ -470,6 +470,7 @@ public final class C {
|
|||||||
value = {
|
value = {
|
||||||
BUFFER_FLAG_KEY_FRAME,
|
BUFFER_FLAG_KEY_FRAME,
|
||||||
BUFFER_FLAG_END_OF_STREAM,
|
BUFFER_FLAG_END_OF_STREAM,
|
||||||
|
BUFFER_FLAG_LAST_SAMPLE,
|
||||||
BUFFER_FLAG_ENCRYPTED,
|
BUFFER_FLAG_ENCRYPTED,
|
||||||
BUFFER_FLAG_DECODE_ONLY
|
BUFFER_FLAG_DECODE_ONLY
|
||||||
})
|
})
|
||||||
@ -482,6 +483,8 @@ public final class C {
|
|||||||
* Flag for empty buffers that signal that the end of the stream was reached.
|
* Flag for empty buffers that signal that the end of the stream was reached.
|
||||||
*/
|
*/
|
||||||
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
||||||
|
/** Indicates that a buffer is known to contain the last media sample of the stream. */
|
||||||
|
public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000
|
||||||
/** Indicates that a buffer is (at least partially) encrypted. */
|
/** Indicates that a buffer is (at least partially) encrypted. */
|
||||||
public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
|
public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
|
||||||
/** Indicates that a buffer should be decoded but not rendered. */
|
/** Indicates that a buffer should be decoded but not rendered. */
|
||||||
@ -533,9 +536,7 @@ public final class C {
|
|||||||
*/
|
*/
|
||||||
public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4
|
public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4
|
||||||
|
|
||||||
/**
|
/** Represents an undetermined language as an ISO 639-2 language code. */
|
||||||
* Represents an undetermined language as an ISO 639 alpha-3 language code.
|
|
||||||
*/
|
|
||||||
public static final String LANGUAGE_UNDETERMINED = "und";
|
public static final String LANGUAGE_UNDETERMINED = "und";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
package com.google.android.exoplayer2;
|
package com.google.android.exoplayer2;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.media.MediaCodec;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
@ -85,15 +86,18 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
|
protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
|
@Nullable private DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
|
||||||
private final @ExtensionRendererMode int extensionRendererMode;
|
@ExtensionRendererMode private int extensionRendererMode;
|
||||||
private final long allowedVideoJoiningTimeMs;
|
private long allowedVideoJoiningTimeMs;
|
||||||
|
private boolean playClearSamplesWithoutKeys;
|
||||||
|
private MediaCodecSelector mediaCodecSelector;
|
||||||
|
|
||||||
/**
|
/** @param context A {@link Context}. */
|
||||||
* @param context A {@link Context}.
|
|
||||||
*/
|
|
||||||
public DefaultRenderersFactory(Context context) {
|
public DefaultRenderersFactory(Context context) {
|
||||||
this(context, EXTENSION_RENDERER_MODE_OFF);
|
this.context = context;
|
||||||
|
extensionRendererMode = EXTENSION_RENDERER_MODE_OFF;
|
||||||
|
allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS;
|
||||||
|
mediaCodecSelector = MediaCodecSelector.DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,19 +112,20 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param context A {@link Context}.
|
* @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link
|
||||||
* @param extensionRendererMode The extension renderer mode, which determines if and how available
|
* #setExtensionRendererMode(int)}.
|
||||||
* extension renderers are used. Note that extensions must be included in the application
|
|
||||||
* build for them to be considered available.
|
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public DefaultRenderersFactory(
|
public DefaultRenderersFactory(
|
||||||
Context context, @ExtensionRendererMode int extensionRendererMode) {
|
Context context, @ExtensionRendererMode int extensionRendererMode) {
|
||||||
this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
|
this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use {@link #DefaultRenderersFactory(Context, int)} and pass {@link
|
* @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link
|
||||||
* DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
|
* #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link
|
||||||
|
* SimpleExoPlayer} or {@link ExoPlayerFactory}.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@ -132,26 +137,22 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param context A {@link Context}.
|
* @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link
|
||||||
* @param extensionRendererMode The extension renderer mode, which determines if and how available
|
* #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}.
|
||||||
* extension renderers are used. Note that extensions must be included in the application
|
|
||||||
* build for them to be considered available.
|
|
||||||
* @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
|
|
||||||
* seamlessly join an ongoing playback.
|
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public DefaultRenderersFactory(
|
public DefaultRenderersFactory(
|
||||||
Context context,
|
Context context,
|
||||||
@ExtensionRendererMode int extensionRendererMode,
|
@ExtensionRendererMode int extensionRendererMode,
|
||||||
long allowedVideoJoiningTimeMs) {
|
long allowedVideoJoiningTimeMs) {
|
||||||
this.context = context;
|
this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs);
|
||||||
this.extensionRendererMode = extensionRendererMode;
|
|
||||||
this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
|
|
||||||
this.drmSessionManager = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use {@link #DefaultRenderersFactory(Context, int, long)} and pass {@link
|
* @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link
|
||||||
* DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
|
* #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}, and pass
|
||||||
|
* {@link DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public DefaultRenderersFactory(
|
public DefaultRenderersFactory(
|
||||||
@ -163,6 +164,70 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
this.extensionRendererMode = extensionRendererMode;
|
this.extensionRendererMode = extensionRendererMode;
|
||||||
this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
|
this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
|
||||||
this.drmSessionManager = drmSessionManager;
|
this.drmSessionManager = drmSessionManager;
|
||||||
|
mediaCodecSelector = MediaCodecSelector.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the extension renderer mode, which determines if and how available extension renderers are
|
||||||
|
* used. Note that extensions must be included in the application build for them to be considered
|
||||||
|
* available.
|
||||||
|
*
|
||||||
|
* <p>The default value is {@link #EXTENSION_RENDERER_MODE_OFF}.
|
||||||
|
*
|
||||||
|
* @param extensionRendererMode The extension renderer mode.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
public DefaultRenderersFactory setExtensionRendererMode(
|
||||||
|
@ExtensionRendererMode int extensionRendererMode) {
|
||||||
|
this.extensionRendererMode = extensionRendererMode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether renderers are permitted to play clear regions of encrypted media prior to having
|
||||||
|
* obtained the keys necessary to decrypt encrypted regions of the media. For encrypted media that
|
||||||
|
* starts with a short clear region, this allows playback to begin in parallel with key
|
||||||
|
* acquisition, which can reduce startup latency.
|
||||||
|
*
|
||||||
|
* <p>The default value is {@code false}.
|
||||||
|
*
|
||||||
|
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
|
||||||
|
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
|
||||||
|
* the media.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
public DefaultRenderersFactory setPlayClearSamplesWithoutKeys(
|
||||||
|
boolean playClearSamplesWithoutKeys) {
|
||||||
|
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers.
|
||||||
|
*
|
||||||
|
* <p>The default value is {@link MediaCodecSelector#DEFAULT}.
|
||||||
|
*
|
||||||
|
* @param mediaCodecSelector The {@link MediaCodecSelector}.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
public DefaultRenderersFactory setMediaCodecSelector(MediaCodecSelector mediaCodecSelector) {
|
||||||
|
this.mediaCodecSelector = mediaCodecSelector;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum duration for which video renderers can attempt to seamlessly join an ongoing
|
||||||
|
* playback.
|
||||||
|
*
|
||||||
|
* <p>The default value is {@link #DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS}.
|
||||||
|
*
|
||||||
|
* @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
|
||||||
|
* seamlessly join an ongoing playback, in milliseconds.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
public DefaultRenderersFactory setAllowedVideoJoiningTimeMs(long allowedVideoJoiningTimeMs) {
|
||||||
|
this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -177,10 +242,26 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
drmSessionManager = this.drmSessionManager;
|
drmSessionManager = this.drmSessionManager;
|
||||||
}
|
}
|
||||||
ArrayList<Renderer> renderersList = new ArrayList<>();
|
ArrayList<Renderer> renderersList = new ArrayList<>();
|
||||||
buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs,
|
buildVideoRenderers(
|
||||||
eventHandler, videoRendererEventListener, extensionRendererMode, renderersList);
|
context,
|
||||||
buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(),
|
extensionRendererMode,
|
||||||
eventHandler, audioRendererEventListener, extensionRendererMode, renderersList);
|
mediaCodecSelector,
|
||||||
|
drmSessionManager,
|
||||||
|
playClearSamplesWithoutKeys,
|
||||||
|
eventHandler,
|
||||||
|
videoRendererEventListener,
|
||||||
|
allowedVideoJoiningTimeMs,
|
||||||
|
renderersList);
|
||||||
|
buildAudioRenderers(
|
||||||
|
context,
|
||||||
|
extensionRendererMode,
|
||||||
|
mediaCodecSelector,
|
||||||
|
drmSessionManager,
|
||||||
|
playClearSamplesWithoutKeys,
|
||||||
|
buildAudioProcessors(),
|
||||||
|
eventHandler,
|
||||||
|
audioRendererEventListener,
|
||||||
|
renderersList);
|
||||||
buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(),
|
buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(),
|
||||||
extensionRendererMode, renderersList);
|
extensionRendererMode, renderersList);
|
||||||
buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(),
|
buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(),
|
||||||
@ -194,27 +275,36 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
* Builds video renderers for use by the player.
|
* Builds video renderers for use by the player.
|
||||||
*
|
*
|
||||||
* @param context The {@link Context} associated with the player.
|
* @param context The {@link Context} associated with the player.
|
||||||
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player
|
* @param extensionRendererMode The extension renderer mode.
|
||||||
* will not be used for DRM protected playbacks.
|
* @param mediaCodecSelector A decoder selector.
|
||||||
* @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video
|
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
|
||||||
* renderers can attempt to seamlessly join an ongoing playback.
|
* not be used for DRM protected playbacks.
|
||||||
|
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
|
||||||
|
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
|
||||||
|
* the media.
|
||||||
* @param eventHandler A handler associated with the main thread's looper.
|
* @param eventHandler A handler associated with the main thread's looper.
|
||||||
* @param eventListener An event listener.
|
* @param eventListener An event listener.
|
||||||
* @param extensionRendererMode The extension renderer mode.
|
* @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
|
||||||
|
* seamlessly join an ongoing playback, in milliseconds.
|
||||||
* @param out An array to which the built renderers should be appended.
|
* @param out An array to which the built renderers should be appended.
|
||||||
*/
|
*/
|
||||||
protected void buildVideoRenderers(Context context,
|
protected void buildVideoRenderers(
|
||||||
|
Context context,
|
||||||
|
@ExtensionRendererMode int extensionRendererMode,
|
||||||
|
MediaCodecSelector mediaCodecSelector,
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
long allowedVideoJoiningTimeMs, Handler eventHandler,
|
boolean playClearSamplesWithoutKeys,
|
||||||
VideoRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
|
Handler eventHandler,
|
||||||
|
VideoRendererEventListener eventListener,
|
||||||
|
long allowedVideoJoiningTimeMs,
|
||||||
ArrayList<Renderer> out) {
|
ArrayList<Renderer> out) {
|
||||||
out.add(
|
out.add(
|
||||||
new MediaCodecVideoRenderer(
|
new MediaCodecVideoRenderer(
|
||||||
context,
|
context,
|
||||||
MediaCodecSelector.DEFAULT,
|
mediaCodecSelector,
|
||||||
allowedVideoJoiningTimeMs,
|
allowedVideoJoiningTimeMs,
|
||||||
drmSessionManager,
|
drmSessionManager,
|
||||||
/* playClearSamplesWithoutKeys= */ false,
|
playClearSamplesWithoutKeys,
|
||||||
eventHandler,
|
eventHandler,
|
||||||
eventListener,
|
eventListener,
|
||||||
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
|
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
|
||||||
@ -233,7 +323,6 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
|
Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
|
||||||
Constructor<?> constructor =
|
Constructor<?> constructor =
|
||||||
clazz.getConstructor(
|
clazz.getConstructor(
|
||||||
boolean.class,
|
|
||||||
long.class,
|
long.class,
|
||||||
android.os.Handler.class,
|
android.os.Handler.class,
|
||||||
com.google.android.exoplayer2.video.VideoRendererEventListener.class,
|
com.google.android.exoplayer2.video.VideoRendererEventListener.class,
|
||||||
@ -242,7 +331,6 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
Renderer renderer =
|
Renderer renderer =
|
||||||
(Renderer)
|
(Renderer)
|
||||||
constructor.newInstance(
|
constructor.newInstance(
|
||||||
true,
|
|
||||||
allowedVideoJoiningTimeMs,
|
allowedVideoJoiningTimeMs,
|
||||||
eventHandler,
|
eventHandler,
|
||||||
eventListener,
|
eventListener,
|
||||||
@ -261,26 +349,35 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
* Builds audio renderers for use by the player.
|
* Builds audio renderers for use by the player.
|
||||||
*
|
*
|
||||||
* @param context The {@link Context} associated with the player.
|
* @param context The {@link Context} associated with the player.
|
||||||
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player
|
* @param extensionRendererMode The extension renderer mode.
|
||||||
* will not be used for DRM protected playbacks.
|
* @param mediaCodecSelector A decoder selector.
|
||||||
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio
|
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
|
||||||
* buffers before output. May be empty.
|
* not be used for DRM protected playbacks.
|
||||||
|
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
|
||||||
|
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
|
||||||
|
* the media.
|
||||||
|
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers
|
||||||
|
* before output. May be empty.
|
||||||
* @param eventHandler A handler to use when invoking event listeners and outputs.
|
* @param eventHandler A handler to use when invoking event listeners and outputs.
|
||||||
* @param eventListener An event listener.
|
* @param eventListener An event listener.
|
||||||
* @param extensionRendererMode The extension renderer mode.
|
|
||||||
* @param out An array to which the built renderers should be appended.
|
* @param out An array to which the built renderers should be appended.
|
||||||
*/
|
*/
|
||||||
protected void buildAudioRenderers(Context context,
|
protected void buildAudioRenderers(
|
||||||
|
Context context,
|
||||||
|
@ExtensionRendererMode int extensionRendererMode,
|
||||||
|
MediaCodecSelector mediaCodecSelector,
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
AudioProcessor[] audioProcessors, Handler eventHandler,
|
boolean playClearSamplesWithoutKeys,
|
||||||
AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
|
AudioProcessor[] audioProcessors,
|
||||||
|
Handler eventHandler,
|
||||||
|
AudioRendererEventListener eventListener,
|
||||||
ArrayList<Renderer> out) {
|
ArrayList<Renderer> out) {
|
||||||
out.add(
|
out.add(
|
||||||
new MediaCodecAudioRenderer(
|
new MediaCodecAudioRenderer(
|
||||||
context,
|
context,
|
||||||
MediaCodecSelector.DEFAULT,
|
mediaCodecSelector,
|
||||||
drmSessionManager,
|
drmSessionManager,
|
||||||
/* playClearSamplesWithoutKeys= */ false,
|
playClearSamplesWithoutKeys,
|
||||||
eventHandler,
|
eventHandler,
|
||||||
eventListener,
|
eventListener,
|
||||||
AudioCapabilities.getCapabilities(context),
|
AudioCapabilities.getCapabilities(context),
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
package com.google.android.exoplayer2;
|
package com.google.android.exoplayer2;
|
||||||
|
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -34,7 +35,7 @@ public final class ExoPlaybackException extends Exception {
|
|||||||
*/
|
*/
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED})
|
@IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED, TYPE_REMOTE})
|
||||||
public @interface Type {}
|
public @interface Type {}
|
||||||
/**
|
/**
|
||||||
* The error occurred loading data from a {@link MediaSource}.
|
* The error occurred loading data from a {@link MediaSource}.
|
||||||
@ -54,6 +55,12 @@ public final class ExoPlaybackException extends Exception {
|
|||||||
* Call {@link #getUnexpectedException()} to retrieve the underlying cause.
|
* Call {@link #getUnexpectedException()} to retrieve the underlying cause.
|
||||||
*/
|
*/
|
||||||
public static final int TYPE_UNEXPECTED = 2;
|
public static final int TYPE_UNEXPECTED = 2;
|
||||||
|
/**
|
||||||
|
* The error occurred in a remote component.
|
||||||
|
*
|
||||||
|
* <p>Call {@link #getMessage()} to retrieve the message associated with the error.
|
||||||
|
*/
|
||||||
|
public static final int TYPE_REMOTE = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
|
* The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
|
||||||
@ -66,7 +73,7 @@ public final class ExoPlaybackException extends Exception {
|
|||||||
*/
|
*/
|
||||||
public final int rendererIndex;
|
public final int rendererIndex;
|
||||||
|
|
||||||
private final Throwable cause;
|
@Nullable private final Throwable cause;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of type {@link #TYPE_SOURCE}.
|
* Creates an instance of type {@link #TYPE_SOURCE}.
|
||||||
@ -99,6 +106,16 @@ public final class ExoPlaybackException extends Exception {
|
|||||||
return new ExoPlaybackException(TYPE_UNEXPECTED, cause, C.INDEX_UNSET);
|
return new ExoPlaybackException(TYPE_UNEXPECTED, cause, C.INDEX_UNSET);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of type {@link #TYPE_REMOTE}.
|
||||||
|
*
|
||||||
|
* @param message The message associated with the error.
|
||||||
|
* @return The created instance.
|
||||||
|
*/
|
||||||
|
public static ExoPlaybackException createForRemote(String message) {
|
||||||
|
return new ExoPlaybackException(TYPE_REMOTE, message);
|
||||||
|
}
|
||||||
|
|
||||||
private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) {
|
private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) {
|
||||||
super(cause);
|
super(cause);
|
||||||
this.type = type;
|
this.type = type;
|
||||||
@ -106,6 +123,13 @@ public final class ExoPlaybackException extends Exception {
|
|||||||
this.rendererIndex = rendererIndex;
|
this.rendererIndex = rendererIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ExoPlaybackException(@Type int type, String message) {
|
||||||
|
super(message);
|
||||||
|
this.type = type;
|
||||||
|
rendererIndex = C.INDEX_UNSET;
|
||||||
|
cause = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the underlying error when {@link #type} is {@link #TYPE_SOURCE}.
|
* Retrieves the underlying error when {@link #type} is {@link #TYPE_SOURCE}.
|
||||||
*
|
*
|
||||||
@ -113,7 +137,7 @@ public final class ExoPlaybackException extends Exception {
|
|||||||
*/
|
*/
|
||||||
public IOException getSourceException() {
|
public IOException getSourceException() {
|
||||||
Assertions.checkState(type == TYPE_SOURCE);
|
Assertions.checkState(type == TYPE_SOURCE);
|
||||||
return (IOException) cause;
|
return (IOException) Assertions.checkNotNull(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,7 +147,7 @@ public final class ExoPlaybackException extends Exception {
|
|||||||
*/
|
*/
|
||||||
public Exception getRendererException() {
|
public Exception getRendererException() {
|
||||||
Assertions.checkState(type == TYPE_RENDERER);
|
Assertions.checkState(type == TYPE_RENDERER);
|
||||||
return (Exception) cause;
|
return (Exception) Assertions.checkNotNull(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,7 +157,7 @@ public final class ExoPlaybackException extends Exception {
|
|||||||
*/
|
*/
|
||||||
public RuntimeException getUnexpectedException() {
|
public RuntimeException getUnexpectedException() {
|
||||||
Assertions.checkState(type == TYPE_UNEXPECTED);
|
Assertions.checkState(type == TYPE_UNEXPECTED);
|
||||||
return (RuntimeException) cause;
|
return (RuntimeException) Assertions.checkNotNull(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,10 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
|||||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||||
import com.google.android.exoplayer2.source.ClippingMediaSource;
|
import com.google.android.exoplayer2.source.ClippingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.LoopingMediaSource;
|
import com.google.android.exoplayer2.source.LoopingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||||
import com.google.android.exoplayer2.text.TextRenderer;
|
import com.google.android.exoplayer2.text.TextRenderer;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
@ -48,7 +48,7 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
|
|||||||
* <li>A <b>{@link MediaSource}</b> that defines the media to be played, loads the media, and from
|
* <li>A <b>{@link MediaSource}</b> that defines the media to be played, loads the media, and from
|
||||||
* which the loaded media can be read. A MediaSource is injected via {@link
|
* which the loaded media can be read. A MediaSource is injected via {@link
|
||||||
* #prepare(MediaSource)} at the start of playback. The library modules provide default
|
* #prepare(MediaSource)} at the start of playback. The library modules provide default
|
||||||
* implementations for regular media files ({@link ExtractorMediaSource}), DASH
|
* implementations for progressive media files ({@link ProgressiveMediaSource}), DASH
|
||||||
* (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an
|
* (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an
|
||||||
* implementation for loading single media samples ({@link SingleSampleMediaSource}) that's
|
* implementation for loading single media samples ({@link SingleSampleMediaSource}) that's
|
||||||
* most often used for side-loaded subtitle files, and implementations for building more
|
* most often used for side-loaded subtitle files, and implementations for building more
|
||||||
|
@ -58,7 +58,8 @@ public final class ExoPlayerFactory {
|
|||||||
LoadControl loadControl,
|
LoadControl loadControl,
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
|
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
|
||||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode);
|
RenderersFactory renderersFactory =
|
||||||
|
new DefaultRenderersFactory(context).setExtensionRendererMode(extensionRendererMode);
|
||||||
return newSimpleInstance(
|
return newSimpleInstance(
|
||||||
context, renderersFactory, trackSelector, loadControl, drmSessionManager);
|
context, renderersFactory, trackSelector, loadControl, drmSessionManager);
|
||||||
}
|
}
|
||||||
@ -88,7 +89,9 @@ public final class ExoPlayerFactory {
|
|||||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
|
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
|
||||||
long allowedVideoJoiningTimeMs) {
|
long allowedVideoJoiningTimeMs) {
|
||||||
RenderersFactory renderersFactory =
|
RenderersFactory renderersFactory =
|
||||||
new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs);
|
new DefaultRenderersFactory(context)
|
||||||
|
.setExtensionRendererMode(extensionRendererMode)
|
||||||
|
.setAllowedVideoJoiningTimeMs(allowedVideoJoiningTimeMs);
|
||||||
return newSimpleInstance(
|
return newSimpleInstance(
|
||||||
context, renderersFactory, trackSelector, loadControl, drmSessionManager);
|
context, renderersFactory, trackSelector, loadControl, drmSessionManager);
|
||||||
}
|
}
|
||||||
|
@ -1376,12 +1376,34 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!queue.updateQueuedPeriods(playingPeriodId, rendererPositionUs)) {
|
if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) {
|
||||||
seekToCurrentPosition(/* sendDiscontinuity= */ false);
|
seekToCurrentPosition(/* sendDiscontinuity= */ false);
|
||||||
}
|
}
|
||||||
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
|
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long getMaxRendererReadPositionUs() {
|
||||||
|
MediaPeriodHolder readingHolder = queue.getReadingPeriod();
|
||||||
|
if (readingHolder == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long maxReadPositionUs = readingHolder.getRendererOffset();
|
||||||
|
for (int i = 0; i < renderers.length; i++) {
|
||||||
|
if (renderers[i].getState() == Renderer.STATE_DISABLED
|
||||||
|
|| renderers[i].getStream() != readingHolder.sampleStreams[i]) {
|
||||||
|
// Ignore disabled renderers and renderers with sample streams from previous periods.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long readingPositionUs = renderers[i].getReadingPositionUs();
|
||||||
|
if (readingPositionUs == C.TIME_END_OF_SOURCE) {
|
||||||
|
return C.TIME_END_OF_SOURCE;
|
||||||
|
} else {
|
||||||
|
maxReadPositionUs = Math.max(readingPositionUs, maxReadPositionUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxReadPositionUs;
|
||||||
|
}
|
||||||
|
|
||||||
private void handleSourceInfoRefreshEndedPlayback() {
|
private void handleSourceInfoRefreshEndedPlayback() {
|
||||||
setState(Player.STATE_ENDED);
|
setState(Player.STATE_ENDED);
|
||||||
// Reset, but retain the source so that it can still be used should a seek occur.
|
// Reset, but retain the source so that it can still be used should a seek occur.
|
||||||
|
@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
|
|||||||
|
|
||||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||||
public static final String VERSION = "2.9.3";
|
public static final String VERSION = "2.9.5";
|
||||||
|
|
||||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.3";
|
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.5";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the library expressed as an integer, for example 1002003.
|
* The version of the library expressed as an integer, for example 1002003.
|
||||||
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
|
|||||||
* integer version 123045006 (123-045-006).
|
* integer version 123045006 (123-045-006).
|
||||||
*/
|
*/
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final int VERSION_INT = 2009003;
|
public static final int VERSION_INT = 2009005;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||||
|
@ -159,7 +159,7 @@ public final class Format implements Parcelable {
|
|||||||
@C.SelectionFlags
|
@C.SelectionFlags
|
||||||
public final int selectionFlags;
|
public final int selectionFlags;
|
||||||
|
|
||||||
/** The language, or null if unknown or not applicable. */
|
/** The language as ISO 639-2/T three-letter code, or null if unknown or not applicable. */
|
||||||
public final @Nullable String language;
|
public final @Nullable String language;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -932,7 +932,7 @@ public final class Format implements Parcelable {
|
|||||||
this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay;
|
this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay;
|
||||||
this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding;
|
this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding;
|
||||||
this.selectionFlags = selectionFlags;
|
this.selectionFlags = selectionFlags;
|
||||||
this.language = language;
|
this.language = Util.normalizeLanguageCode(language);
|
||||||
this.accessibilityChannel = accessibilityChannel;
|
this.accessibilityChannel = accessibilityChannel;
|
||||||
this.subsampleOffsetUs = subsampleOffsetUs;
|
this.subsampleOffsetUs = subsampleOffsetUs;
|
||||||
this.initializationData =
|
this.initializationData =
|
||||||
|
@ -89,7 +89,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
this.info = info;
|
this.info = info;
|
||||||
sampleStreams = new SampleStream[rendererCapabilities.length];
|
sampleStreams = new SampleStream[rendererCapabilities.length];
|
||||||
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
|
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
|
||||||
mediaPeriod = createMediaPeriod(info.id, mediaSource, allocator);
|
mediaPeriod =
|
||||||
|
createMediaPeriod(
|
||||||
|
info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -294,7 +296,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
public void release() {
|
public void release() {
|
||||||
disableTrackSelectionsInResult();
|
disableTrackSelectionsInResult();
|
||||||
trackSelectorResult = null;
|
trackSelectorResult = null;
|
||||||
releaseMediaPeriod(info.id, mediaSource, mediaPeriod);
|
releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -399,24 +401,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
|
|
||||||
/** Returns a media period corresponding to the given {@code id}. */
|
/** Returns a media period corresponding to the given {@code id}. */
|
||||||
private static MediaPeriod createMediaPeriod(
|
private static MediaPeriod createMediaPeriod(
|
||||||
MediaPeriodId id, MediaSource mediaSource, Allocator allocator) {
|
MediaPeriodId id,
|
||||||
MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator);
|
MediaSource mediaSource,
|
||||||
if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) {
|
Allocator allocator,
|
||||||
|
long startPositionUs,
|
||||||
|
long endPositionUs) {
|
||||||
|
MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs);
|
||||||
|
if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
|
||||||
mediaPeriod =
|
mediaPeriod =
|
||||||
new ClippingMediaPeriod(
|
new ClippingMediaPeriod(
|
||||||
mediaPeriod,
|
mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, endPositionUs);
|
||||||
/* enableInitialDiscontinuity= */ true,
|
|
||||||
/* startUs= */ 0,
|
|
||||||
id.endPositionUs);
|
|
||||||
}
|
}
|
||||||
return mediaPeriod;
|
return mediaPeriod;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */
|
/** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */
|
||||||
private static void releaseMediaPeriod(
|
private static void releaseMediaPeriod(
|
||||||
MediaPeriodId id, MediaSource mediaSource, MediaPeriod mediaPeriod) {
|
long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) {
|
||||||
try {
|
try {
|
||||||
if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) {
|
if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
|
||||||
mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
|
mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
|
||||||
} else {
|
} else {
|
||||||
mediaSource.releasePeriod(mediaPeriod);
|
mediaSource.releasePeriod(mediaPeriod);
|
||||||
|
@ -33,7 +33,14 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
*/
|
*/
|
||||||
public final long contentPositionUs;
|
public final long contentPositionUs;
|
||||||
/**
|
/**
|
||||||
* The duration of the media period, like {@link MediaPeriodId#endPositionUs} but with {@link
|
* The end position to which the media period's content is clipped in order to play a following ad
|
||||||
|
* group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if this
|
||||||
|
* media period is an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll ad
|
||||||
|
* follows at the end of this content media period.
|
||||||
|
*/
|
||||||
|
public final long endPositionUs;
|
||||||
|
/**
|
||||||
|
* The duration of the media period, like {@link #endPositionUs} but with {@link
|
||||||
* C#TIME_END_OF_SOURCE} and {@link C#TIME_UNSET} resolved to the timeline period duration if
|
* C#TIME_END_OF_SOURCE} and {@link C#TIME_UNSET} resolved to the timeline period duration if
|
||||||
* known.
|
* known.
|
||||||
*/
|
*/
|
||||||
@ -53,23 +60,48 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
MediaPeriodId id,
|
MediaPeriodId id,
|
||||||
long startPositionUs,
|
long startPositionUs,
|
||||||
long contentPositionUs,
|
long contentPositionUs,
|
||||||
|
long endPositionUs,
|
||||||
long durationUs,
|
long durationUs,
|
||||||
boolean isLastInTimelinePeriod,
|
boolean isLastInTimelinePeriod,
|
||||||
boolean isFinal) {
|
boolean isFinal) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.startPositionUs = startPositionUs;
|
this.startPositionUs = startPositionUs;
|
||||||
this.contentPositionUs = contentPositionUs;
|
this.contentPositionUs = contentPositionUs;
|
||||||
|
this.endPositionUs = endPositionUs;
|
||||||
this.durationUs = durationUs;
|
this.durationUs = durationUs;
|
||||||
this.isLastInTimelinePeriod = isLastInTimelinePeriod;
|
this.isLastInTimelinePeriod = isLastInTimelinePeriod;
|
||||||
this.isFinal = isFinal;
|
this.isFinal = isFinal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a copy of this instance with the start position set to the specified value. */
|
/**
|
||||||
|
* Returns a copy of this instance with the start position set to the specified value. May return
|
||||||
|
* the same instance if nothing changed.
|
||||||
|
*/
|
||||||
public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) {
|
public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) {
|
||||||
return new MediaPeriodInfo(
|
return startPositionUs == this.startPositionUs
|
||||||
|
? this
|
||||||
|
: new MediaPeriodInfo(
|
||||||
id,
|
id,
|
||||||
startPositionUs,
|
startPositionUs,
|
||||||
contentPositionUs,
|
contentPositionUs,
|
||||||
|
endPositionUs,
|
||||||
|
durationUs,
|
||||||
|
isLastInTimelinePeriod,
|
||||||
|
isFinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of this instance with the content position set to the specified value. May
|
||||||
|
* return the same instance if nothing changed.
|
||||||
|
*/
|
||||||
|
public MediaPeriodInfo copyWithContentPositionUs(long contentPositionUs) {
|
||||||
|
return contentPositionUs == this.contentPositionUs
|
||||||
|
? this
|
||||||
|
: new MediaPeriodInfo(
|
||||||
|
id,
|
||||||
|
startPositionUs,
|
||||||
|
contentPositionUs,
|
||||||
|
endPositionUs,
|
||||||
durationUs,
|
durationUs,
|
||||||
isLastInTimelinePeriod,
|
isLastInTimelinePeriod,
|
||||||
isFinal);
|
isFinal);
|
||||||
@ -86,6 +118,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
MediaPeriodInfo that = (MediaPeriodInfo) o;
|
MediaPeriodInfo that = (MediaPeriodInfo) o;
|
||||||
return startPositionUs == that.startPositionUs
|
return startPositionUs == that.startPositionUs
|
||||||
&& contentPositionUs == that.contentPositionUs
|
&& contentPositionUs == that.contentPositionUs
|
||||||
|
&& endPositionUs == that.endPositionUs
|
||||||
&& durationUs == that.durationUs
|
&& durationUs == that.durationUs
|
||||||
&& isLastInTimelinePeriod == that.isLastInTimelinePeriod
|
&& isLastInTimelinePeriod == that.isLastInTimelinePeriod
|
||||||
&& isFinal == that.isFinal
|
&& isFinal == that.isFinal
|
||||||
@ -98,6 +131,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
result = 31 * result + id.hashCode();
|
result = 31 * result + id.hashCode();
|
||||||
result = 31 * result + (int) startPositionUs;
|
result = 31 * result + (int) startPositionUs;
|
||||||
result = 31 * result + (int) contentPositionUs;
|
result = 31 * result + (int) contentPositionUs;
|
||||||
|
result = 31 * result + (int) endPositionUs;
|
||||||
result = 31 * result + (int) durationUs;
|
result = 31 * result + (int) durationUs;
|
||||||
result = 31 * result + (isLastInTimelinePeriod ? 1 : 0);
|
result = 31 * result + (isLastInTimelinePeriod ? 1 : 0);
|
||||||
result = 31 * result + (isFinal ? 1 : 0);
|
result = 31 * result + (isFinal ? 1 : 0);
|
||||||
|
@ -61,8 +61,8 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(MediaPeriodId, long)} to update the
|
* Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long, long)} to update the queued
|
||||||
* queued media periods to take into account the new timeline.
|
* media periods to take into account the new timeline.
|
||||||
*/
|
*/
|
||||||
public void setTimeline(Timeline timeline) {
|
public void setTimeline(Timeline timeline) {
|
||||||
this.timeline = timeline;
|
this.timeline = timeline;
|
||||||
@ -292,54 +292,56 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||||||
* current playback position. The method assumes that the first media period in the queue is still
|
* current playback position. The method assumes that the first media period in the queue is still
|
||||||
* consistent with the new timeline.
|
* consistent with the new timeline.
|
||||||
*
|
*
|
||||||
* @param playingPeriodId The current playing media period identifier.
|
|
||||||
* @param rendererPositionUs The current renderer position in microseconds.
|
* @param rendererPositionUs The current renderer position in microseconds.
|
||||||
|
* @param maxRendererReadPositionUs The maximum renderer position up to which renderers have read
|
||||||
|
* the current reading media period in microseconds, or {@link C#TIME_END_OF_SOURCE} if they
|
||||||
|
* have read to the end.
|
||||||
* @return Whether the timeline change has been handled completely.
|
* @return Whether the timeline change has been handled completely.
|
||||||
*/
|
*/
|
||||||
public boolean updateQueuedPeriods(MediaPeriodId playingPeriodId, long rendererPositionUs) {
|
public boolean updateQueuedPeriods(long rendererPositionUs, long maxRendererReadPositionUs) {
|
||||||
// TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline
|
// TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline
|
||||||
// is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be
|
// is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be
|
||||||
// handled here.
|
// handled here.
|
||||||
int periodIndex = timeline.getIndexOfPeriod(playingPeriodId.periodUid);
|
|
||||||
// The front period is either playing now, or is being loaded and will become the playing
|
|
||||||
// period.
|
|
||||||
MediaPeriodHolder previousPeriodHolder = null;
|
MediaPeriodHolder previousPeriodHolder = null;
|
||||||
MediaPeriodHolder periodHolder = getFrontPeriod();
|
MediaPeriodHolder periodHolder = getFrontPeriod();
|
||||||
while (periodHolder != null) {
|
while (periodHolder != null) {
|
||||||
|
MediaPeriodInfo oldPeriodInfo = periodHolder.info;
|
||||||
|
|
||||||
|
// Get period info based on new timeline.
|
||||||
|
MediaPeriodInfo newPeriodInfo;
|
||||||
if (previousPeriodHolder == null) {
|
if (previousPeriodHolder == null) {
|
||||||
long previousDurationUs = periodHolder.info.durationUs;
|
// The id and start position of the first period have already been verified by
|
||||||
periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info);
|
// ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline
|
||||||
if (!canKeepAfterMediaPeriodHolder(periodHolder, previousDurationUs)) {
|
// and isLastInPeriod flags.
|
||||||
return !removeAfter(periodHolder);
|
newPeriodInfo = getUpdatedMediaPeriodInfo(oldPeriodInfo);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Check this period holder still follows the previous one, based on the new timeline.
|
newPeriodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);
|
||||||
if (periodIndex == C.INDEX_UNSET
|
if (newPeriodInfo == null) {
|
||||||
|| !periodHolder.uid.equals(timeline.getUidOfPeriod(periodIndex))) {
|
|
||||||
// The holder uid is inconsistent with the new timeline.
|
|
||||||
return !removeAfter(previousPeriodHolder);
|
|
||||||
}
|
|
||||||
MediaPeriodInfo periodInfo =
|
|
||||||
getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);
|
|
||||||
if (periodInfo == null) {
|
|
||||||
// We've loaded a next media period that is not in the new timeline.
|
// We've loaded a next media period that is not in the new timeline.
|
||||||
return !removeAfter(previousPeriodHolder);
|
return !removeAfter(previousPeriodHolder);
|
||||||
}
|
}
|
||||||
// Update the period holder.
|
if (!canKeepMediaPeriodHolder(oldPeriodInfo, newPeriodInfo)) {
|
||||||
periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info);
|
// The new media period has a different id or start position.
|
||||||
// Check the media period information matches the new timeline.
|
|
||||||
if (!canKeepMediaPeriodHolder(periodHolder, periodInfo)) {
|
|
||||||
return !removeAfter(previousPeriodHolder);
|
return !removeAfter(previousPeriodHolder);
|
||||||
} else if (!canKeepAfterMediaPeriodHolder(periodHolder, periodInfo.durationUs)) {
|
|
||||||
return !removeAfter(periodHolder);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (periodHolder.info.isLastInTimelinePeriod) {
|
// Use new period info, but keep old content position.
|
||||||
// Move on to the next timeline period index, if there is one.
|
periodHolder.info = newPeriodInfo.copyWithContentPositionUs(oldPeriodInfo.contentPositionUs);
|
||||||
periodIndex =
|
|
||||||
timeline.getNextPeriodIndex(
|
if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) {
|
||||||
periodIndex, period, window, repeatMode, shuffleModeEnabled);
|
// The period duration changed. Remove all subsequent periods and check whether we read
|
||||||
|
// beyond the new duration.
|
||||||
|
long newDurationInRendererTime =
|
||||||
|
newPeriodInfo.durationUs == C.TIME_UNSET
|
||||||
|
? Long.MAX_VALUE
|
||||||
|
: periodHolder.toRendererTime(newPeriodInfo.durationUs);
|
||||||
|
boolean isReadingAndReadBeyondNewDuration =
|
||||||
|
periodHolder == reading
|
||||||
|
&& (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE
|
||||||
|
|| maxRendererReadPositionUs >= newDurationInRendererTime);
|
||||||
|
boolean readingPeriodRemoved = removeAfter(periodHolder);
|
||||||
|
return !readingPeriodRemoved && !isReadingAndReadBeyondNewDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
previousPeriodHolder = periodHolder;
|
previousPeriodHolder = periodHolder;
|
||||||
@ -364,13 +366,14 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||||||
long durationUs =
|
long durationUs =
|
||||||
id.isAd()
|
id.isAd()
|
||||||
? period.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup)
|
? period.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup)
|
||||||
: (id.endPositionUs == C.TIME_UNSET || id.endPositionUs == C.TIME_END_OF_SOURCE
|
: (info.endPositionUs == C.TIME_UNSET || info.endPositionUs == C.TIME_END_OF_SOURCE
|
||||||
? period.getDurationUs()
|
? period.getDurationUs()
|
||||||
: id.endPositionUs);
|
: info.endPositionUs);
|
||||||
return new MediaPeriodInfo(
|
return new MediaPeriodInfo(
|
||||||
id,
|
id,
|
||||||
info.startPositionUs,
|
info.startPositionUs,
|
||||||
info.contentPositionUs,
|
info.contentPositionUs,
|
||||||
|
info.endPositionUs,
|
||||||
durationUs,
|
durationUs,
|
||||||
isLastInPeriod,
|
isLastInPeriod,
|
||||||
isLastInTimeline);
|
isLastInTimeline);
|
||||||
@ -409,11 +412,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||||||
int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
|
int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
|
||||||
if (adGroupIndex == C.INDEX_UNSET) {
|
if (adGroupIndex == C.INDEX_UNSET) {
|
||||||
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
|
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
|
||||||
long endPositionUs =
|
return new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);
|
||||||
nextAdGroupIndex == C.INDEX_UNSET
|
|
||||||
? C.TIME_UNSET
|
|
||||||
: period.getAdGroupTimeUs(nextAdGroupIndex);
|
|
||||||
return new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs);
|
|
||||||
} else {
|
} else {
|
||||||
int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex);
|
int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex);
|
||||||
return new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
|
return new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
|
||||||
@ -465,22 +464,18 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether {@code periodHolder} can be kept for playing the media period described by
|
* Returns whether a period described by {@code oldInfo} can be kept for playing the media period
|
||||||
* {@code info}.
|
* described by {@code newInfo}.
|
||||||
*/
|
*/
|
||||||
private boolean canKeepMediaPeriodHolder(MediaPeriodHolder periodHolder, MediaPeriodInfo info) {
|
private boolean canKeepMediaPeriodHolder(MediaPeriodInfo oldInfo, MediaPeriodInfo newInfo) {
|
||||||
MediaPeriodInfo periodHolderInfo = periodHolder.info;
|
return oldInfo.startPositionUs == newInfo.startPositionUs && oldInfo.id.equals(newInfo.id);
|
||||||
return periodHolderInfo.startPositionUs == info.startPositionUs
|
|
||||||
&& periodHolderInfo.id.equals(info.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether periods after {@code periodHolder} can be kept for playing given its previous
|
* Returns whether a duration change of a period is compatible with keeping the following periods.
|
||||||
* duration.
|
|
||||||
*/
|
*/
|
||||||
private boolean canKeepAfterMediaPeriodHolder(
|
private boolean areDurationsCompatible(long previousDurationUs, long newDurationUs) {
|
||||||
MediaPeriodHolder periodHolder, long previousDurationUs) {
|
return previousDurationUs == C.TIME_UNSET || previousDurationUs == newDurationUs;
|
||||||
return previousDurationUs == C.TIME_UNSET || previousDurationUs == periodHolder.info.durationUs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -645,7 +640,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Play the next ad group if it's available.
|
// Play the next ad group if it's available.
|
||||||
int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.id.endPositionUs);
|
int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs);
|
||||||
if (nextAdGroupIndex == C.INDEX_UNSET) {
|
if (nextAdGroupIndex == C.INDEX_UNSET) {
|
||||||
// The next ad group can't be played. Play content from the previous end position instead.
|
// The next ad group can't be played. Play content from the previous end position instead.
|
||||||
return getMediaPeriodInfoForContent(
|
return getMediaPeriodInfoForContent(
|
||||||
@ -703,6 +698,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||||||
id,
|
id,
|
||||||
startPositionUs,
|
startPositionUs,
|
||||||
contentPositionUs,
|
contentPositionUs,
|
||||||
|
/* endPositionUs= */ C.TIME_UNSET,
|
||||||
durationUs,
|
durationUs,
|
||||||
/* isLastInTimelinePeriod= */ false,
|
/* isLastInTimelinePeriod= */ false,
|
||||||
/* isFinal= */ false);
|
/* isFinal= */ false);
|
||||||
@ -711,13 +707,13 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||||||
private MediaPeriodInfo getMediaPeriodInfoForContent(
|
private MediaPeriodInfo getMediaPeriodInfoForContent(
|
||||||
Object periodUid, long startPositionUs, long windowSequenceNumber) {
|
Object periodUid, long startPositionUs, long windowSequenceNumber) {
|
||||||
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
|
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
|
||||||
|
MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);
|
||||||
|
boolean isLastInPeriod = isLastInPeriod(id);
|
||||||
|
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
|
||||||
long endPositionUs =
|
long endPositionUs =
|
||||||
nextAdGroupIndex != C.INDEX_UNSET
|
nextAdGroupIndex != C.INDEX_UNSET
|
||||||
? period.getAdGroupTimeUs(nextAdGroupIndex)
|
? period.getAdGroupTimeUs(nextAdGroupIndex)
|
||||||
: C.TIME_UNSET;
|
: C.TIME_UNSET;
|
||||||
MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs);
|
|
||||||
boolean isLastInPeriod = isLastInPeriod(id);
|
|
||||||
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
|
|
||||||
long durationUs =
|
long durationUs =
|
||||||
endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE
|
endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE
|
||||||
? period.durationUs
|
? period.durationUs
|
||||||
@ -726,13 +722,14 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||||||
id,
|
id,
|
||||||
startPositionUs,
|
startPositionUs,
|
||||||
/* contentPositionUs= */ C.TIME_UNSET,
|
/* contentPositionUs= */ C.TIME_UNSET,
|
||||||
|
endPositionUs,
|
||||||
durationUs,
|
durationUs,
|
||||||
isLastInPeriod,
|
isLastInPeriod,
|
||||||
isLastInTimeline);
|
isLastInTimeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isLastInPeriod(MediaPeriodId id) {
|
private boolean isLastInPeriod(MediaPeriodId id) {
|
||||||
return !id.isAd() && id.endPositionUs == C.TIME_UNSET;
|
return !id.isAd() && id.nextAdGroupIndex == C.INDEX_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {
|
private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {
|
||||||
|
@ -122,6 +122,11 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getReadingPositionUs() {
|
||||||
|
return C.TIME_END_OF_SOURCE;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void setCurrentStreamFinal() {
|
public final void setCurrentStreamFinal() {
|
||||||
streamIsFinal = true;
|
streamIsFinal = true;
|
||||||
|
@ -160,6 +160,16 @@ public interface Renderer extends PlayerMessage.Target {
|
|||||||
*/
|
*/
|
||||||
boolean hasReadStreamToEnd();
|
boolean hasReadStreamToEnd();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the playback position up to which the renderer has read samples from the current {@link
|
||||||
|
* SampleStream}, in microseconds, or {@link C#TIME_END_OF_SOURCE} if the renderer has read the
|
||||||
|
* current {@link SampleStream} to the end.
|
||||||
|
*
|
||||||
|
* <p>This method may be called when the renderer is in the following states: {@link
|
||||||
|
* #STATE_ENABLED}, {@link #STATE_STARTED}.
|
||||||
|
*/
|
||||||
|
long getReadingPositionUs();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signals to the renderer that the current {@link SampleStream} will be the final one supplied
|
* Signals to the renderer that the current {@link SampleStream} will be the final one supplied
|
||||||
* before it is next disabled or reset.
|
* before it is next disabled or reset.
|
||||||
|
@ -63,7 +63,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
* An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can
|
* An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can
|
||||||
* be obtained from {@link ExoPlayerFactory}.
|
* be obtained from {@link ExoPlayerFactory}.
|
||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
|
||||||
public class SimpleExoPlayer extends BasePlayer
|
public class SimpleExoPlayer extends BasePlayer
|
||||||
implements ExoPlayer,
|
implements ExoPlayer,
|
||||||
Player.AudioComponent,
|
Player.AudioComponent,
|
||||||
@ -94,25 +93,25 @@ public class SimpleExoPlayer extends BasePlayer
|
|||||||
|
|
||||||
private final AudioFocusManager audioFocusManager;
|
private final AudioFocusManager audioFocusManager;
|
||||||
|
|
||||||
private Format videoFormat;
|
@Nullable private Format videoFormat;
|
||||||
private Format audioFormat;
|
@Nullable private Format audioFormat;
|
||||||
|
|
||||||
private Surface surface;
|
@Nullable private Surface surface;
|
||||||
private boolean ownsSurface;
|
private boolean ownsSurface;
|
||||||
private @C.VideoScalingMode int videoScalingMode;
|
private @C.VideoScalingMode int videoScalingMode;
|
||||||
private SurfaceHolder surfaceHolder;
|
@Nullable private SurfaceHolder surfaceHolder;
|
||||||
private TextureView textureView;
|
@Nullable private TextureView textureView;
|
||||||
private int surfaceWidth;
|
private int surfaceWidth;
|
||||||
private int surfaceHeight;
|
private int surfaceHeight;
|
||||||
private DecoderCounters videoDecoderCounters;
|
@Nullable private DecoderCounters videoDecoderCounters;
|
||||||
private DecoderCounters audioDecoderCounters;
|
@Nullable private DecoderCounters audioDecoderCounters;
|
||||||
private int audioSessionId;
|
private int audioSessionId;
|
||||||
private AudioAttributes audioAttributes;
|
private AudioAttributes audioAttributes;
|
||||||
private float audioVolume;
|
private float audioVolume;
|
||||||
private MediaSource mediaSource;
|
@Nullable private MediaSource mediaSource;
|
||||||
private List<Cue> currentCues;
|
private List<Cue> currentCues;
|
||||||
private VideoFrameMetadataListener videoFrameMetadataListener;
|
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
|
||||||
private CameraMotionListener cameraMotionListener;
|
@Nullable private CameraMotionListener cameraMotionListener;
|
||||||
private boolean hasNotifiedFullWrongThreadWarning;
|
private boolean hasNotifiedFullWrongThreadWarning;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -558,30 +557,26 @@ public class SimpleExoPlayer extends BasePlayer
|
|||||||
setPlaybackParameters(playbackParameters);
|
setPlaybackParameters(playbackParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the video format currently being played, or null if no video is being played. */
|
||||||
* Returns the video format currently being played, or null if no video is being played.
|
@Nullable
|
||||||
*/
|
|
||||||
public Format getVideoFormat() {
|
public Format getVideoFormat() {
|
||||||
return videoFormat;
|
return videoFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the audio format currently being played, or null if no audio is being played. */
|
||||||
* Returns the audio format currently being played, or null if no audio is being played.
|
@Nullable
|
||||||
*/
|
|
||||||
public Format getAudioFormat() {
|
public Format getAudioFormat() {
|
||||||
return audioFormat;
|
return audioFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns {@link DecoderCounters} for video, or null if no video is being played. */
|
||||||
* Returns {@link DecoderCounters} for video, or null if no video is being played.
|
@Nullable
|
||||||
*/
|
|
||||||
public DecoderCounters getVideoDecoderCounters() {
|
public DecoderCounters getVideoDecoderCounters() {
|
||||||
return videoDecoderCounters;
|
return videoDecoderCounters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns {@link DecoderCounters} for audio, or null if no audio is being played. */
|
||||||
* Returns {@link DecoderCounters} for audio, or null if no audio is being played.
|
@Nullable
|
||||||
*/
|
|
||||||
public DecoderCounters getAudioDecoderCounters() {
|
public DecoderCounters getAudioDecoderCounters() {
|
||||||
return audioDecoderCounters;
|
return audioDecoderCounters;
|
||||||
}
|
}
|
||||||
@ -1053,7 +1048,8 @@ public class SimpleExoPlayer extends BasePlayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object getCurrentManifest() {
|
@Nullable
|
||||||
|
public Object getCurrentManifest() {
|
||||||
verifyApplicationThread();
|
verifyApplicationThread();
|
||||||
return player.getCurrentManifest();
|
return player.getCurrentManifest();
|
||||||
}
|
}
|
||||||
|
@ -129,12 +129,13 @@ public class AnalyticsCollector
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the player for which data will be collected. Must only be called if no player has been set
|
* Sets the player for which data will be collected. Must only be called if no player has been set
|
||||||
* yet.
|
* yet or the current player is idle.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player} for which data will be collected.
|
* @param player The {@link Player} for which data will be collected.
|
||||||
*/
|
*/
|
||||||
public void setPlayer(Player player) {
|
public void setPlayer(Player player) {
|
||||||
Assertions.checkState(this.player == null);
|
Assertions.checkState(
|
||||||
|
this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty());
|
||||||
this.player = Assertions.checkNotNull(player);
|
this.player = Assertions.checkNotNull(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,7 +489,10 @@ public class AnalyticsCollector
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void onPlayerError(ExoPlaybackException error) {
|
public final void onPlayerError(ExoPlaybackException error) {
|
||||||
EventTime eventTime = generatePlayingMediaPeriodEventTime();
|
EventTime eventTime =
|
||||||
|
error.type == ExoPlaybackException.TYPE_SOURCE
|
||||||
|
? generateLoadingMediaPeriodEventTime()
|
||||||
|
: generatePlayingMediaPeriodEventTime();
|
||||||
for (AnalyticsListener listener : listeners) {
|
for (AnalyticsListener listener : listeners) {
|
||||||
listener.onPlayerError(eventTime, error);
|
listener.onPlayerError(eventTime, error);
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,7 @@ public interface AudioRendererEventListener {
|
|||||||
* Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
|
* Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
|
||||||
*/
|
*/
|
||||||
public void disabled(final DecoderCounters counters) {
|
public void disabled(final DecoderCounters counters) {
|
||||||
|
counters.ensureUpdated();
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
handler.post(
|
handler.post(
|
||||||
() -> {
|
() -> {
|
||||||
|
@ -418,7 +418,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
isInputPcm = Util.isEncodingLinearPcm(inputEncoding);
|
isInputPcm = Util.isEncodingLinearPcm(inputEncoding);
|
||||||
shouldConvertHighResIntPcmToFloat =
|
shouldConvertHighResIntPcmToFloat =
|
||||||
enableConvertHighResIntPcmToFloat
|
enableConvertHighResIntPcmToFloat
|
||||||
&& supportsOutput(channelCount, C.ENCODING_PCM_32BIT)
|
&& supportsOutput(channelCount, C.ENCODING_PCM_FLOAT)
|
||||||
&& Util.isEncodingHighResolutionIntegerPcm(inputEncoding);
|
&& Util.isEncodingHighResolutionIntegerPcm(inputEncoding);
|
||||||
if (isInputPcm) {
|
if (isInputPcm) {
|
||||||
pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount);
|
pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount);
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
package com.google.android.exoplayer2.audio;
|
package com.google.android.exoplayer2.audio;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCrypto;
|
import android.media.MediaCrypto;
|
||||||
@ -66,7 +65,6 @@ import java.util.List;
|
|||||||
* underlying audio track.
|
* underlying audio track.
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
|
||||||
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
|
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -548,7 +546,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
try {
|
try {
|
||||||
super.onDisabled();
|
super.onDisabled();
|
||||||
} finally {
|
} finally {
|
||||||
decoderCounters.ensureUpdated();
|
|
||||||
eventDispatcher.disabled(decoderCounters);
|
eventDispatcher.disabled(decoderCounters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,8 +106,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
? extends AudioDecoderException> decoder;
|
? extends AudioDecoderException> decoder;
|
||||||
private DecoderInputBuffer inputBuffer;
|
private DecoderInputBuffer inputBuffer;
|
||||||
private SimpleOutputBuffer outputBuffer;
|
private SimpleOutputBuffer outputBuffer;
|
||||||
private DrmSession<ExoMediaCrypto> drmSession;
|
@Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
|
||||||
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
@Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;
|
||||||
|
|
||||||
@ReinitializationState private int decoderReinitializationState;
|
@ReinitializationState private int decoderReinitializationState;
|
||||||
private boolean decoderReceivedBuffers;
|
private boolean decoderReceivedBuffers;
|
||||||
@ -462,12 +462,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||||
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
@DrmSession.State int drmSessionState = decoderDrmSession.getState();
|
||||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
|
||||||
}
|
}
|
||||||
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
||||||
}
|
}
|
||||||
@ -568,27 +568,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
audioTrackNeedsConfigure = true;
|
audioTrackNeedsConfigure = true;
|
||||||
waitingForKeys = false;
|
waitingForKeys = false;
|
||||||
try {
|
try {
|
||||||
|
setSourceDrmSession(null);
|
||||||
releaseDecoder();
|
releaseDecoder();
|
||||||
audioSink.reset();
|
audioSink.reset();
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
|
||||||
if (drmSession != null) {
|
|
||||||
drmSessionManager.releaseSession(drmSession);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
|
|
||||||
drmSessionManager.releaseSession(pendingDrmSession);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
drmSession = null;
|
|
||||||
pendingDrmSession = null;
|
|
||||||
decoderCounters.ensureUpdated();
|
|
||||||
eventDispatcher.disabled(decoderCounters);
|
eventDispatcher.disabled(decoderCounters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
|
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
|
||||||
@ -615,12 +601,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
drmSession = pendingDrmSession;
|
setDecoderDrmSession(sourceDrmSession);
|
||||||
|
|
||||||
ExoMediaCrypto mediaCrypto = null;
|
ExoMediaCrypto mediaCrypto = null;
|
||||||
if (drmSession != null) {
|
if (decoderDrmSession != null) {
|
||||||
mediaCrypto = drmSession.getMediaCrypto();
|
mediaCrypto = decoderDrmSession.getMediaCrypto();
|
||||||
if (mediaCrypto == null) {
|
if (mediaCrypto == null) {
|
||||||
DrmSessionException drmError = drmSession.getError();
|
DrmSessionException drmError = decoderDrmSession.getError();
|
||||||
if (drmError != null) {
|
if (drmError != null) {
|
||||||
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
|
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
|
||||||
// input format causes the session to be replaced before it's used.
|
// input format causes the session to be replaced before it's used.
|
||||||
@ -646,17 +633,34 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void releaseDecoder() {
|
private void releaseDecoder() {
|
||||||
if (decoder == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
|
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||||
|
decoderReceivedBuffers = false;
|
||||||
|
if (decoder != null) {
|
||||||
decoder.release();
|
decoder.release();
|
||||||
decoder = null;
|
decoder = null;
|
||||||
decoderCounters.decoderReleaseCount++;
|
decoderCounters.decoderReleaseCount++;
|
||||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
}
|
||||||
decoderReceivedBuffers = false;
|
setDecoderDrmSession(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
|
||||||
|
DrmSession<ExoMediaCrypto> previous = sourceDrmSession;
|
||||||
|
sourceDrmSession = session;
|
||||||
|
releaseDrmSessionIfUnused(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
|
||||||
|
DrmSession<ExoMediaCrypto> previous = decoderDrmSession;
|
||||||
|
decoderDrmSession = session;
|
||||||
|
releaseDrmSessionIfUnused(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseDrmSessionIfUnused(@Nullable DrmSession<ExoMediaCrypto> session) {
|
||||||
|
if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
|
||||||
|
drmSessionManager.releaseSession(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
|
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
|
||||||
@ -671,13 +675,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
throw ExoPlaybackException.createForRenderer(
|
throw ExoPlaybackException.createForRenderer(
|
||||||
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
||||||
}
|
}
|
||||||
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(),
|
DrmSession<ExoMediaCrypto> session =
|
||||||
inputFormat.drmInitData);
|
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
|
||||||
if (pendingDrmSession == drmSession) {
|
if (session == decoderDrmSession || session == sourceDrmSession) {
|
||||||
drmSessionManager.releaseSession(pendingDrmSession);
|
// We already had this session. The manager must be reference counting, so release it once
|
||||||
|
// to get the count attributed to this renderer back down to 1.
|
||||||
|
drmSessionManager.releaseSession(session);
|
||||||
}
|
}
|
||||||
|
setSourceDrmSession(session);
|
||||||
} else {
|
} else {
|
||||||
pendingDrmSession = null;
|
setSourceDrmSession(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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.database;
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides {@link SQLiteDatabase} instances to ExoPlayer components, which may read and write
|
||||||
|
* tables prefixed with {@link #TABLE_PREFIX}.
|
||||||
|
*/
|
||||||
|
public interface DatabaseProvider {
|
||||||
|
|
||||||
|
/** Prefix for tables that can be read and written by ExoPlayer components. */
|
||||||
|
String TABLE_PREFIX = "ExoPlayer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and/or opens a database that will be used for reading and writing.
|
||||||
|
*
|
||||||
|
* <p>Once opened successfully, the database is cached, so you can call this method every time you
|
||||||
|
* need to write to the database. Errors such as bad permissions or a full disk may cause this
|
||||||
|
* method to fail, but future attempts may succeed if the problem is fixed.
|
||||||
|
*
|
||||||
|
* @throws SQLiteException If the database cannot be opened for writing.
|
||||||
|
* @return A read/write database object.
|
||||||
|
*/
|
||||||
|
SQLiteDatabase getWritableDatabase();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and/or opens a database. This will be the same object returned by {@link
|
||||||
|
* #getWritableDatabase()} unless some problem, such as a full disk, requires the database to be
|
||||||
|
* opened read-only. In that case, a read-only database object will be returned. If the problem is
|
||||||
|
* fixed, a future call to {@link #getWritableDatabase()} may succeed, in which case the read-only
|
||||||
|
* database object will be closed and the read/write object will be returned in the future.
|
||||||
|
*
|
||||||
|
* <p>Once opened successfully, the database is cached, so you can call this method every time you
|
||||||
|
* need to read from the database.
|
||||||
|
*
|
||||||
|
* @throws SQLiteException If the database cannot be opened.
|
||||||
|
* @return A database object valid until {@link #getWritableDatabase()} is called.
|
||||||
|
*/
|
||||||
|
SQLiteDatabase getReadableDatabase();
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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.database;
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
|
||||||
|
/** A {@link DatabaseProvider} that provides instances obtained from a {@link SQLiteOpenHelper}. */
|
||||||
|
public final class DefaultDatabaseProvider implements DatabaseProvider {
|
||||||
|
|
||||||
|
private final SQLiteOpenHelper sqliteOpenHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sqliteOpenHelper An {@link SQLiteOpenHelper} from which to obtain database instances.
|
||||||
|
*/
|
||||||
|
public DefaultDatabaseProvider(SQLiteOpenHelper sqliteOpenHelper) {
|
||||||
|
this.sqliteOpenHelper = sqliteOpenHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLiteDatabase getWritableDatabase() {
|
||||||
|
return sqliteOpenHelper.getWritableDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLiteDatabase getReadableDatabase() {
|
||||||
|
return sqliteOpenHelper.getReadableDatabase();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* 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.database;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.ContextWrapper;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.DatabaseErrorHandler;
|
||||||
|
import android.database.SQLException;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link SQLiteOpenHelper} that provides instances of a standalone ExoPlayer database.
|
||||||
|
*
|
||||||
|
* <p>Suitable for use by applications that do not already have their own database, or which would
|
||||||
|
* prefer to keep ExoPlayer tables isolated in their own database. Other applications should prefer
|
||||||
|
* to use {@link DefaultDatabaseProvider} with their own {@link SQLiteOpenHelper}.
|
||||||
|
*/
|
||||||
|
public final class ExoDatabaseProvider extends SQLiteOpenHelper implements DatabaseProvider {
|
||||||
|
|
||||||
|
/** The file name used for the standalone ExoPlayer database. */
|
||||||
|
public static final String DATABASE_NAME = "exoplayer_internal.db";
|
||||||
|
|
||||||
|
private static final int VERSION = 1;
|
||||||
|
private static final String TAG = "ExoDatabaseProvider";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides instances of the database located by passing {@link #DATABASE_NAME} to {@link
|
||||||
|
* Context#getDatabasePath(String)}.
|
||||||
|
*
|
||||||
|
* @param context Any context.
|
||||||
|
*/
|
||||||
|
public ExoDatabaseProvider(Context context) {
|
||||||
|
super(context.getApplicationContext(), DATABASE_NAME, /* factory= */ null, VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides instances of the database located at the specified file.
|
||||||
|
*
|
||||||
|
* @param file The database file.
|
||||||
|
*/
|
||||||
|
public ExoDatabaseProvider(File file) {
|
||||||
|
super(new DatabaseFileProvidingContext(file), file.getName(), /* factory= */ null, VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
// Features create their own tables.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
// Features handle their own upgrades.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
wipeDatabase(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a best effort to wipe the existing database. The wipe may be incomplete if the database
|
||||||
|
* contains foreign key constraints.
|
||||||
|
*/
|
||||||
|
private static void wipeDatabase(SQLiteDatabase db) {
|
||||||
|
String[] columns = {"type", "name"};
|
||||||
|
try (Cursor cursor =
|
||||||
|
db.query(
|
||||||
|
"sqlite_master",
|
||||||
|
columns,
|
||||||
|
/* selection= */ null,
|
||||||
|
/* selectionArgs= */ null,
|
||||||
|
/* groupBy= */ null,
|
||||||
|
/* having= */ null,
|
||||||
|
/* orderBy= */ null)) {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
String type = cursor.getString(0);
|
||||||
|
String name = cursor.getString(1);
|
||||||
|
if (!"sqlite_sequence".equals(name)) {
|
||||||
|
// If it's not an SQL-controlled entity, drop it
|
||||||
|
String sql = "DROP " + type + " IF EXISTS " + name;
|
||||||
|
try {
|
||||||
|
db.execSQL(sql);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
Log.e(TAG, "Error executing " + sql, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This is fragile. Stop using it if/when SQLiteOpenHelper can be instantiated without a
|
||||||
|
// context [Internal ref: b/123351819], or by injecting a Context into all components that need
|
||||||
|
// to instantiate an ExoDatabaseProvider.
|
||||||
|
/** A {@link Context} that implements methods called by {@link SQLiteOpenHelper}. */
|
||||||
|
private static class DatabaseFileProvidingContext extends ContextWrapper {
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
|
||||||
|
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||||
|
public DatabaseFileProvidingContext(File file) {
|
||||||
|
super(/* base= */ null);
|
||||||
|
this.file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getDatabasePath(String name) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLiteDatabase openOrCreateDatabase(
|
||||||
|
String name, int mode, SQLiteDatabase.CursorFactory factory) {
|
||||||
|
return openOrCreateDatabase(name, mode, factory, /* errorHandler= */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||||
|
public SQLiteDatabase openOrCreateDatabase(
|
||||||
|
String name,
|
||||||
|
int mode,
|
||||||
|
SQLiteDatabase.CursorFactory factory,
|
||||||
|
@Nullable DatabaseErrorHandler errorHandler) {
|
||||||
|
File databasePath = getDatabasePath(name);
|
||||||
|
int flags = SQLiteDatabase.CREATE_IF_NECESSARY;
|
||||||
|
if ((mode & MODE_ENABLE_WRITE_AHEAD_LOGGING) != 0) {
|
||||||
|
flags |= SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING;
|
||||||
|
}
|
||||||
|
if ((mode & MODE_NO_LOCALIZED_COLLATORS) != 0) {
|
||||||
|
flags |= SQLiteDatabase.NO_LOCALIZED_COLLATORS;
|
||||||
|
}
|
||||||
|
return SQLiteDatabase.openDatabase(databasePath.getPath(), factory, flags, errorHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* 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.database;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.DatabaseUtils;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
|
import android.support.annotation.VisibleForTesting;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility methods for accessing versions of ExoPlayer database components. This allows them to be
|
||||||
|
* versioned independently to the version of the containing database.
|
||||||
|
*/
|
||||||
|
public final class VersionTable {
|
||||||
|
|
||||||
|
/** Returned by {@link #getVersion(SQLiteDatabase, int)} if the version is unset. */
|
||||||
|
public static final int VERSION_UNSET = -1;
|
||||||
|
/** Version of tables used for offline functionality. */
|
||||||
|
public static final int FEATURE_OFFLINE = 0;
|
||||||
|
/** Version of tables used for cache content metadata. */
|
||||||
|
public static final int FEATURE_CACHE_CONTENT_METADATA = 1;
|
||||||
|
/** Version of tables used for cache file metadata. */
|
||||||
|
public static final int FEATURE_CACHE_FILE_METADATA = 2;
|
||||||
|
|
||||||
|
private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Versions";
|
||||||
|
|
||||||
|
private static final String COLUMN_FEATURE = "feature";
|
||||||
|
private static final String COLUMN_VERSION = "version";
|
||||||
|
|
||||||
|
private static final String SQL_CREATE_TABLE_IF_NOT_EXISTS =
|
||||||
|
"CREATE TABLE IF NOT EXISTS "
|
||||||
|
+ TABLE_NAME
|
||||||
|
+ " ("
|
||||||
|
+ COLUMN_FEATURE
|
||||||
|
+ " INTEGER PRIMARY KEY NOT NULL,"
|
||||||
|
+ COLUMN_VERSION
|
||||||
|
+ " INTEGER NOT NULL)";
|
||||||
|
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@IntDef({FEATURE_OFFLINE, FEATURE_CACHE_CONTENT_METADATA, FEATURE_CACHE_FILE_METADATA})
|
||||||
|
private @interface Feature {}
|
||||||
|
|
||||||
|
private VersionTable() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the version of tables belonging to the specified feature.
|
||||||
|
*
|
||||||
|
* @param writableDatabase The database to update.
|
||||||
|
* @param feature The feature.
|
||||||
|
* @param version The version.
|
||||||
|
*/
|
||||||
|
public static void setVersion(
|
||||||
|
SQLiteDatabase writableDatabase, @Feature int feature, int version) {
|
||||||
|
writableDatabase.execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS);
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COLUMN_FEATURE, feature);
|
||||||
|
values.put(COLUMN_VERSION, version);
|
||||||
|
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the version of tables belonging to the specified feature, or {@link #VERSION_UNSET} if
|
||||||
|
* no version information is available.
|
||||||
|
*
|
||||||
|
* @param database The database to query.
|
||||||
|
* @param feature The feature.
|
||||||
|
*/
|
||||||
|
public static int getVersion(SQLiteDatabase database, @Feature int feature) {
|
||||||
|
if (!tableExists(database, TABLE_NAME)) {
|
||||||
|
return VERSION_UNSET;
|
||||||
|
}
|
||||||
|
String selection = COLUMN_FEATURE + " = ?";
|
||||||
|
String[] selectionArgs = {Integer.toString(feature)};
|
||||||
|
try (Cursor cursor =
|
||||||
|
database.query(
|
||||||
|
TABLE_NAME,
|
||||||
|
new String[] {COLUMN_VERSION},
|
||||||
|
selection,
|
||||||
|
selectionArgs,
|
||||||
|
/* groupBy= */ null,
|
||||||
|
/* having= */ null,
|
||||||
|
/* orderBy= */ null)) {
|
||||||
|
if (cursor.getCount() == 0) {
|
||||||
|
return VERSION_UNSET;
|
||||||
|
}
|
||||||
|
cursor.moveToNext();
|
||||||
|
return cursor.getInt(/* COLUMN_VERSION index */ 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
/* package */ static boolean tableExists(SQLiteDatabase readableDatabase, String tableName) {
|
||||||
|
long count =
|
||||||
|
DatabaseUtils.queryNumEntries(
|
||||||
|
readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName});
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
}
|
@ -62,7 +62,7 @@ public final class CryptoInfo {
|
|||||||
private final PatternHolderV24 patternHolder;
|
private final PatternHolderV24 patternHolder;
|
||||||
|
|
||||||
public CryptoInfo() {
|
public CryptoInfo() {
|
||||||
frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null;
|
frameworkCryptoInfo = new android.media.MediaCodec.CryptoInfo();
|
||||||
patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null;
|
patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,34 +79,8 @@ public final class CryptoInfo {
|
|||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
this.encryptedBlocks = encryptedBlocks;
|
this.encryptedBlocks = encryptedBlocks;
|
||||||
this.clearBlocks = clearBlocks;
|
this.clearBlocks = clearBlocks;
|
||||||
if (Util.SDK_INT >= 16) {
|
// Update frameworkCryptoInfo fields directly because CryptoInfo.set performs an unnecessary
|
||||||
updateFrameworkCryptoInfoV16();
|
// object allocation on Android N.
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
|
|
||||||
* <p>
|
|
||||||
* Successive calls to this method on a single {@link CryptoInfo} will return the same instance.
|
|
||||||
* Changes to the {@link CryptoInfo} will be reflected in the returned object. The return object
|
|
||||||
* should not be modified directly.
|
|
||||||
*
|
|
||||||
* @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
|
|
||||||
*/
|
|
||||||
@TargetApi(16)
|
|
||||||
public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {
|
|
||||||
return frameworkCryptoInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(16)
|
|
||||||
private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() {
|
|
||||||
return new android.media.MediaCodec.CryptoInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(16)
|
|
||||||
private void updateFrameworkCryptoInfoV16() {
|
|
||||||
// Update fields directly because the framework's CryptoInfo.set performs an unnecessary object
|
|
||||||
// allocation on Android N.
|
|
||||||
frameworkCryptoInfo.numSubSamples = numSubSamples;
|
frameworkCryptoInfo.numSubSamples = numSubSamples;
|
||||||
frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData;
|
frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData;
|
||||||
frameworkCryptoInfo.numBytesOfEncryptedData = numBytesOfEncryptedData;
|
frameworkCryptoInfo.numBytesOfEncryptedData = numBytesOfEncryptedData;
|
||||||
@ -118,6 +92,25 @@ public final class CryptoInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
|
||||||
|
*
|
||||||
|
* <p>Successive calls to this method on a single {@link CryptoInfo} will return the same
|
||||||
|
* instance. Changes to the {@link CryptoInfo} will be reflected in the returned object. The
|
||||||
|
* return object should not be modified directly.
|
||||||
|
*
|
||||||
|
* @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
|
||||||
|
*/
|
||||||
|
public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfo() {
|
||||||
|
return frameworkCryptoInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use {@link #getFrameworkCryptoInfo()}. */
|
||||||
|
@Deprecated
|
||||||
|
public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {
|
||||||
|
return getFrameworkCryptoInfo();
|
||||||
|
}
|
||||||
|
|
||||||
@TargetApi(24)
|
@TargetApi(24)
|
||||||
private static final class PatternHolderV24 {
|
private static final class PatternHolderV24 {
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.drm;
|
package com.google.android.exoplayer2.drm;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.media.MediaDrm;
|
import android.media.MediaDrm;
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
@ -27,7 +26,6 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* A DRM session.
|
* A DRM session.
|
||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
|
||||||
public interface DrmSession<T extends ExoMediaCrypto> {
|
public interface DrmSession<T extends ExoMediaCrypto> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,14 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.drm;
|
package com.google.android.exoplayer2.drm;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages a DRM session.
|
* Manages a DRM session.
|
||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
|
||||||
public interface DrmSessionManager<T extends ExoMediaCrypto> {
|
public interface DrmSessionManager<T extends ExoMediaCrypto> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,14 +15,5 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.drm;
|
package com.google.android.exoplayer2.drm;
|
||||||
|
|
||||||
/**
|
/** An opaque {@link android.media.MediaCrypto} equivalent. */
|
||||||
* An opaque {@link android.media.MediaCrypto} equivalent.
|
public interface ExoMediaCrypto {}
|
||||||
*/
|
|
||||||
public interface ExoMediaCrypto {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see android.media.MediaCrypto#requiresSecureDecoderComponent(String)
|
|
||||||
*/
|
|
||||||
boolean requiresSecureDecoderComponent(String mimeType);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -265,11 +265,9 @@ public interface ExoMediaDrm<T extends ExoMediaCrypto> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see android.media.MediaCrypto#MediaCrypto(UUID, byte[])
|
* @see android.media.MediaCrypto#MediaCrypto(UUID, byte[])
|
||||||
*
|
* @param sessionId The DRM session ID.
|
||||||
* @param initData Opaque initialization data specific to the crypto scheme.
|
|
||||||
* @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.
|
* @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.
|
||||||
* @throws MediaCryptoException If the instance can't be created.
|
* @throws MediaCryptoException If the instance can't be created.
|
||||||
*/
|
*/
|
||||||
T createMediaCrypto(byte[] initData) throws MediaCryptoException;
|
T createMediaCrypto(byte[] sessionId) throws MediaCryptoException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,50 +15,35 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.drm;
|
package com.google.android.exoplayer2.drm;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.media.MediaCrypto;
|
import android.media.MediaCrypto;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link ExoMediaCrypto} implementation that wraps the framework {@link MediaCrypto}.
|
* An {@link ExoMediaCrypto} implementation that contains the necessary information to build or
|
||||||
|
* update a framework {@link MediaCrypto}.
|
||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
|
||||||
public final class FrameworkMediaCrypto implements ExoMediaCrypto {
|
public final class FrameworkMediaCrypto implements ExoMediaCrypto {
|
||||||
|
|
||||||
private final MediaCrypto mediaCrypto;
|
/** The DRM scheme UUID. */
|
||||||
private final boolean forceAllowInsecureDecoderComponents;
|
public final UUID uuid;
|
||||||
|
/** The DRM session id. */
|
||||||
|
public final byte[] sessionId;
|
||||||
|
/**
|
||||||
|
* Whether to allow use of insecure decoder components even if the underlying platform says
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public final boolean forceAllowInsecureDecoderComponents;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mediaCrypto The {@link MediaCrypto} to wrap.
|
* @param uuid The DRM scheme UUID.
|
||||||
|
* @param sessionId The DRM session id.
|
||||||
|
* @param forceAllowInsecureDecoderComponents Whether to allow use of insecure decoder components
|
||||||
|
* even if the underlying platform says otherwise.
|
||||||
*/
|
*/
|
||||||
public FrameworkMediaCrypto(MediaCrypto mediaCrypto) {
|
public FrameworkMediaCrypto(
|
||||||
this(mediaCrypto, false);
|
UUID uuid, byte[] sessionId, boolean forceAllowInsecureDecoderComponents) {
|
||||||
}
|
this.uuid = uuid;
|
||||||
|
this.sessionId = sessionId;
|
||||||
/**
|
|
||||||
* @param mediaCrypto The {@link MediaCrypto} to wrap.
|
|
||||||
* @param forceAllowInsecureDecoderComponents Whether to force
|
|
||||||
* {@link #requiresSecureDecoderComponent(String)} to return {@code false}, rather than
|
|
||||||
* {@link MediaCrypto#requiresSecureDecoderComponent(String)} of the wrapped
|
|
||||||
* {@link MediaCrypto}.
|
|
||||||
*/
|
|
||||||
public FrameworkMediaCrypto(MediaCrypto mediaCrypto,
|
|
||||||
boolean forceAllowInsecureDecoderComponents) {
|
|
||||||
this.mediaCrypto = Assertions.checkNotNull(mediaCrypto);
|
|
||||||
this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;
|
this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the wrapped {@link MediaCrypto}.
|
|
||||||
*/
|
|
||||||
public MediaCrypto getWrappedMediaCrypto() {
|
|
||||||
return mediaCrypto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean requiresSecureDecoderComponent(String mimeType) {
|
|
||||||
return !forceAllowInsecureDecoderComponents
|
|
||||||
&& mediaCrypto.requiresSecureDecoderComponent(mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ package com.google.android.exoplayer2.drm;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.media.DeniedByServerException;
|
import android.media.DeniedByServerException;
|
||||||
import android.media.MediaCrypto;
|
|
||||||
import android.media.MediaCryptoException;
|
import android.media.MediaCryptoException;
|
||||||
import android.media.MediaDrm;
|
import android.media.MediaDrm;
|
||||||
import android.media.MediaDrmException;
|
import android.media.MediaDrmException;
|
||||||
@ -210,7 +209,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
|
|||||||
boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
|
boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
|
||||||
&& C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
|
&& C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
|
||||||
return new FrameworkMediaCrypto(
|
return new FrameworkMediaCrypto(
|
||||||
new MediaCrypto(adjustUuid(uuid), initData), forceAllowInsecureDecoderComponents);
|
adjustUuid(uuid), initData, forceAllowInsecureDecoderComponents);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) {
|
private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) {
|
||||||
|
@ -64,6 +64,9 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
this.durationUs = durationUs;
|
this.durationUs = durationUs;
|
||||||
sampleCount = offsets.length;
|
sampleCount = offsets.length;
|
||||||
|
if (flags.length > 0) {
|
||||||
|
flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,7 +50,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||||||
FLAG_IGNORE_H264_STREAM,
|
FLAG_IGNORE_H264_STREAM,
|
||||||
FLAG_DETECT_ACCESS_UNITS,
|
FLAG_DETECT_ACCESS_UNITS,
|
||||||
FLAG_IGNORE_SPLICE_INFO_STREAM,
|
FLAG_IGNORE_SPLICE_INFO_STREAM,
|
||||||
FLAG_OVERRIDE_CAPTION_DESCRIPTORS
|
FLAG_OVERRIDE_CAPTION_DESCRIPTORS,
|
||||||
|
FLAG_IGNORE_HDMV_DTS_STREAM
|
||||||
})
|
})
|
||||||
public @interface Flags {}
|
public @interface Flags {}
|
||||||
|
|
||||||
@ -86,6 +87,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||||||
* closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors.
|
* closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors.
|
||||||
*/
|
*/
|
||||||
public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5;
|
public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5;
|
||||||
|
/**
|
||||||
|
* Prevents the creation of {@link DtsReader} instances when receiving {@link
|
||||||
|
* TsExtractor#TS_STREAM_TYPE_HDMV_DTS} as stream type. Enabling this flag prevents a stream type
|
||||||
|
* collision between HDMV DTS audio and SCTE-35 subtitles.
|
||||||
|
*/
|
||||||
|
public static final int FLAG_IGNORE_HDMV_DTS_STREAM = 1 << 6;
|
||||||
|
|
||||||
private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86;
|
private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86;
|
||||||
|
|
||||||
@ -142,8 +149,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||||||
case TsExtractor.TS_STREAM_TYPE_AC3:
|
case TsExtractor.TS_STREAM_TYPE_AC3:
|
||||||
case TsExtractor.TS_STREAM_TYPE_E_AC3:
|
case TsExtractor.TS_STREAM_TYPE_E_AC3:
|
||||||
return new PesReader(new Ac3Reader(esInfo.language));
|
return new PesReader(new Ac3Reader(esInfo.language));
|
||||||
case TsExtractor.TS_STREAM_TYPE_DTS:
|
|
||||||
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
|
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
|
||||||
|
if (isSet(FLAG_IGNORE_HDMV_DTS_STREAM)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Fall through.
|
||||||
|
case TsExtractor.TS_STREAM_TYPE_DTS:
|
||||||
return new PesReader(new DtsReader(esInfo.language));
|
return new PesReader(new DtsReader(esInfo.language));
|
||||||
case TsExtractor.TS_STREAM_TYPE_H262:
|
case TsExtractor.TS_STREAM_TYPE_H262:
|
||||||
return new PesReader(new H262Reader(buildUserDataReader(esInfo)));
|
return new PesReader(new H262Reader(buildUserDataReader(esInfo)));
|
||||||
|
@ -100,7 +100,7 @@ public interface TsPayloadReader {
|
|||||||
public final byte[] initializationData;
|
public final byte[] initializationData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param language The ISO 639-2 three character language.
|
* @param language The ISO 639-2 three-letter language code.
|
||||||
* @param type The subtitling type.
|
* @param type The subtitling type.
|
||||||
* @param initializationData The composition and ancillary page ids.
|
* @param initializationData The composition and ancillary page ids.
|
||||||
*/
|
*/
|
||||||
|
@ -31,7 +31,6 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
|||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
/** Information about a {@link MediaCodec} for a given mime type. */
|
/** Information about a {@link MediaCodec} for a given mime type. */
|
||||||
@TargetApi(16)
|
|
||||||
@SuppressWarnings("InlinedApi")
|
@SuppressWarnings("InlinedApi")
|
||||||
public final class MediaCodecInfo {
|
public final class MediaCodecInfo {
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import android.media.MediaCodec;
|
|||||||
import android.media.MediaCodec.CodecException;
|
import android.media.MediaCodec.CodecException;
|
||||||
import android.media.MediaCodec.CryptoException;
|
import android.media.MediaCodec.CryptoException;
|
||||||
import android.media.MediaCrypto;
|
import android.media.MediaCrypto;
|
||||||
|
import android.media.MediaCryptoException;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
@ -57,7 +58,6 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* An abstract renderer that uses {@link MediaCodec} to decode samples for rendering.
|
* An abstract renderer that uses {@link MediaCodec} to decode samples for rendering.
|
||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
|
||||||
public abstract class MediaCodecRenderer extends BaseRenderer {
|
public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -239,14 +239,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
|
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({DRAIN_ACTION_NONE, DRAIN_ACTION_FLUSH, DRAIN_ACTION_REINITIALIZE})
|
@IntDef({
|
||||||
|
DRAIN_ACTION_NONE,
|
||||||
|
DRAIN_ACTION_FLUSH,
|
||||||
|
DRAIN_ACTION_UPDATE_DRM_SESSION,
|
||||||
|
DRAIN_ACTION_REINITIALIZE
|
||||||
|
})
|
||||||
private @interface DrainAction {}
|
private @interface DrainAction {}
|
||||||
/** No special action should be taken. */
|
/** No special action should be taken. */
|
||||||
private static final int DRAIN_ACTION_NONE = 0;
|
private static final int DRAIN_ACTION_NONE = 0;
|
||||||
/** The codec should be flushed. */
|
/** The codec should be flushed. */
|
||||||
private static final int DRAIN_ACTION_FLUSH = 1;
|
private static final int DRAIN_ACTION_FLUSH = 1;
|
||||||
/** The codec should be re-initialized. */
|
/** The codec should be flushed and updated to use the pending DRM session. */
|
||||||
private static final int DRAIN_ACTION_REINITIALIZE = 2;
|
private static final int DRAIN_ACTION_UPDATE_DRM_SESSION = 2;
|
||||||
|
/** The codec should be reinitialized. */
|
||||||
|
private static final int DRAIN_ACTION_REINITIALIZE = 3;
|
||||||
|
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@ -287,13 +294,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
private final DecoderInputBuffer flagsOnlyBuffer;
|
private final DecoderInputBuffer flagsOnlyBuffer;
|
||||||
private final FormatHolder formatHolder;
|
private final FormatHolder formatHolder;
|
||||||
private final TimedValueQueue<Format> formatQueue;
|
private final TimedValueQueue<Format> formatQueue;
|
||||||
private final List<Long> decodeOnlyPresentationTimestamps;
|
private final ArrayList<Long> decodeOnlyPresentationTimestamps;
|
||||||
private final MediaCodec.BufferInfo outputBufferInfo;
|
private final MediaCodec.BufferInfo outputBufferInfo;
|
||||||
|
|
||||||
@Nullable private Format inputFormat;
|
@Nullable private Format inputFormat;
|
||||||
private Format outputFormat;
|
private Format outputFormat;
|
||||||
private DrmSession<FrameworkMediaCrypto> drmSession;
|
@Nullable private DrmSession<FrameworkMediaCrypto> codecDrmSession;
|
||||||
private DrmSession<FrameworkMediaCrypto> pendingDrmSession;
|
@Nullable private DrmSession<FrameworkMediaCrypto> sourceDrmSession;
|
||||||
|
@Nullable private MediaCrypto mediaCrypto;
|
||||||
|
private boolean mediaCryptoRequiresSecureDecoder;
|
||||||
private long renderTimeLimitMs;
|
private long renderTimeLimitMs;
|
||||||
private float rendererOperatingRate;
|
private float rendererOperatingRate;
|
||||||
@Nullable private MediaCodec codec;
|
@Nullable private MediaCodec codec;
|
||||||
@ -356,7 +365,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
boolean playClearSamplesWithoutKeys,
|
boolean playClearSamplesWithoutKeys,
|
||||||
float assumedMinimumCodecOperatingRate) {
|
float assumedMinimumCodecOperatingRate) {
|
||||||
super(trackType);
|
super(trackType);
|
||||||
Assertions.checkState(Util.SDK_INT >= 16);
|
|
||||||
this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector);
|
this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector);
|
||||||
this.drmSessionManager = drmSessionManager;
|
this.drmSessionManager = drmSessionManager;
|
||||||
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||||
@ -457,29 +465,36 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
drmSession = pendingDrmSession;
|
setCodecDrmSession(sourceDrmSession);
|
||||||
|
|
||||||
String mimeType = inputFormat.sampleMimeType;
|
String mimeType = inputFormat.sampleMimeType;
|
||||||
MediaCrypto wrappedMediaCrypto = null;
|
if (codecDrmSession != null) {
|
||||||
boolean drmSessionRequiresSecureDecoder = false;
|
|
||||||
if (drmSession != null) {
|
|
||||||
FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto();
|
|
||||||
if (mediaCrypto == null) {
|
if (mediaCrypto == null) {
|
||||||
DrmSessionException drmError = drmSession.getError();
|
FrameworkMediaCrypto sessionMediaCrypto = codecDrmSession.getMediaCrypto();
|
||||||
|
if (sessionMediaCrypto == null) {
|
||||||
|
DrmSessionException drmError = codecDrmSession.getError();
|
||||||
if (drmError != null) {
|
if (drmError != null) {
|
||||||
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
|
// Continue for now. We may be able to avoid failure if the session recovers, or if a
|
||||||
// input format causes the session to be replaced before it's used.
|
// new input format causes the session to be replaced before it's used.
|
||||||
} else {
|
} else {
|
||||||
// The drm session isn't open yet.
|
// The drm session isn't open yet.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
|
try {
|
||||||
drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
|
mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId);
|
||||||
|
} catch (MediaCryptoException e) {
|
||||||
|
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||||
|
}
|
||||||
|
mediaCryptoRequiresSecureDecoder =
|
||||||
|
!sessionMediaCrypto.forceAllowInsecureDecoderComponents
|
||||||
|
&& mediaCrypto.requiresSecureDecoderComponent(mimeType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
|
if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
|
||||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
@DrmSession.State int drmSessionState = codecDrmSession.getState();
|
||||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
|
||||||
} else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
|
} else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
|
||||||
// Wait for keys.
|
// Wait for keys.
|
||||||
return;
|
return;
|
||||||
@ -488,7 +503,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
maybeInitCodecWithFallback(wrappedMediaCrypto, drmSessionRequiresSecureDecoder);
|
maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder);
|
||||||
} catch (DecoderInitializationException e) {
|
} catch (DecoderInitializationException e) {
|
||||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||||
}
|
}
|
||||||
@ -537,7 +552,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
||||||
inputStreamEnded = false;
|
inputStreamEnded = false;
|
||||||
outputStreamEnded = false;
|
outputStreamEnded = false;
|
||||||
flushOrReinitCodec();
|
flushOrReinitializeCodec();
|
||||||
formatQueue.clear();
|
formatQueue.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,7 +567,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
@Override
|
@Override
|
||||||
protected void onDisabled() {
|
protected void onDisabled() {
|
||||||
inputFormat = null;
|
inputFormat = null;
|
||||||
if (drmSession != null || pendingDrmSession != null) {
|
if (sourceDrmSession != null || codecDrmSession != null) {
|
||||||
// TODO: Do something better with this case.
|
// TODO: Do something better with this case.
|
||||||
onReset();
|
onReset();
|
||||||
} else {
|
} else {
|
||||||
@ -565,26 +580,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
try {
|
try {
|
||||||
releaseCodec();
|
releaseCodec();
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
setSourceDrmSession(null);
|
||||||
if (drmSession != null) {
|
|
||||||
drmSessionManager.releaseSession(drmSession);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
|
|
||||||
drmSessionManager.releaseSession(pendingDrmSession);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
drmSession = null;
|
|
||||||
pendingDrmSession = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void releaseCodec() {
|
protected void releaseCodec() {
|
||||||
availableCodecInfos = null;
|
availableCodecInfos = null;
|
||||||
if (codec != null) {
|
|
||||||
codecInfo = null;
|
codecInfo = null;
|
||||||
codecFormat = null;
|
codecFormat = null;
|
||||||
resetInputBuffer();
|
resetInputBuffer();
|
||||||
@ -593,22 +594,25 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
waitingForKeys = false;
|
waitingForKeys = false;
|
||||||
codecHotswapDeadlineMs = C.TIME_UNSET;
|
codecHotswapDeadlineMs = C.TIME_UNSET;
|
||||||
decodeOnlyPresentationTimestamps.clear();
|
decodeOnlyPresentationTimestamps.clear();
|
||||||
|
try {
|
||||||
|
if (codec != null) {
|
||||||
decoderCounters.decoderReleaseCount++;
|
decoderCounters.decoderReleaseCount++;
|
||||||
try {
|
try {
|
||||||
codec.stop();
|
codec.stop();
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
|
||||||
codec.release();
|
codec.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
codec = null;
|
codec = null;
|
||||||
if (drmSession != null && pendingDrmSession != drmSession) {
|
|
||||||
try {
|
try {
|
||||||
drmSessionManager.releaseSession(drmSession);
|
if (mediaCrypto != null) {
|
||||||
|
mediaCrypto.release();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
drmSession = null;
|
mediaCrypto = null;
|
||||||
}
|
mediaCryptoRequiresSecureDecoder = false;
|
||||||
}
|
setCodecDrmSession(null);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -680,12 +684,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
* <p>The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link
|
* <p>The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link
|
||||||
* #maybeInitCodec()} if the codec needs to be re-instantiated.
|
* #maybeInitCodec()} if the codec needs to be re-instantiated.
|
||||||
*
|
*
|
||||||
|
* @return Whether the codec was released and reinitialized, rather than being flushed.
|
||||||
* @throws ExoPlaybackException If an error occurs re-instantiating the codec.
|
* @throws ExoPlaybackException If an error occurs re-instantiating the codec.
|
||||||
*/
|
*/
|
||||||
protected final void flushOrReinitCodec() throws ExoPlaybackException {
|
protected final boolean flushOrReinitializeCodec() throws ExoPlaybackException {
|
||||||
if (flushOrReleaseCodec()) {
|
boolean released = flushOrReleaseCodec();
|
||||||
|
if (released) {
|
||||||
maybeInitCodec();
|
maybeInitCodec();
|
||||||
}
|
}
|
||||||
|
return released;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -729,18 +736,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void maybeInitCodecWithFallback(
|
private void maybeInitCodecWithFallback(
|
||||||
MediaCrypto crypto, boolean drmSessionRequiresSecureDecoder)
|
MediaCrypto crypto, boolean mediaCryptoRequiresSecureDecoder)
|
||||||
throws DecoderInitializationException {
|
throws DecoderInitializationException {
|
||||||
if (availableCodecInfos == null) {
|
if (availableCodecInfos == null) {
|
||||||
try {
|
try {
|
||||||
availableCodecInfos =
|
availableCodecInfos =
|
||||||
new ArrayDeque<>(getAvailableCodecInfos(drmSessionRequiresSecureDecoder));
|
new ArrayDeque<>(getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder));
|
||||||
preferredDecoderInitializationException = null;
|
preferredDecoderInitializationException = null;
|
||||||
} catch (DecoderQueryException e) {
|
} catch (DecoderQueryException e) {
|
||||||
throw new DecoderInitializationException(
|
throw new DecoderInitializationException(
|
||||||
inputFormat,
|
inputFormat,
|
||||||
e,
|
e,
|
||||||
drmSessionRequiresSecureDecoder,
|
mediaCryptoRequiresSecureDecoder,
|
||||||
DecoderInitializationException.DECODER_QUERY_ERROR);
|
DecoderInitializationException.DECODER_QUERY_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -749,7 +756,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
throw new DecoderInitializationException(
|
throw new DecoderInitializationException(
|
||||||
inputFormat,
|
inputFormat,
|
||||||
/* cause= */ null,
|
/* cause= */ null,
|
||||||
drmSessionRequiresSecureDecoder,
|
mediaCryptoRequiresSecureDecoder,
|
||||||
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);
|
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -768,7 +775,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
availableCodecInfos.removeFirst();
|
availableCodecInfos.removeFirst();
|
||||||
DecoderInitializationException exception =
|
DecoderInitializationException exception =
|
||||||
new DecoderInitializationException(
|
new DecoderInitializationException(
|
||||||
inputFormat, e, drmSessionRequiresSecureDecoder, codecInfo.name);
|
inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo.name);
|
||||||
if (preferredDecoderInitializationException == null) {
|
if (preferredDecoderInitializationException == null) {
|
||||||
preferredDecoderInitializationException = exception;
|
preferredDecoderInitializationException = exception;
|
||||||
} else {
|
} else {
|
||||||
@ -784,11 +791,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
availableCodecInfos = null;
|
availableCodecInfos = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<MediaCodecInfo> getAvailableCodecInfos(boolean drmSessionRequiresSecureDecoder)
|
private List<MediaCodecInfo> getAvailableCodecInfos(boolean mediaCryptoRequiresSecureDecoder)
|
||||||
throws DecoderQueryException {
|
throws DecoderQueryException {
|
||||||
List<MediaCodecInfo> codecInfos =
|
List<MediaCodecInfo> codecInfos =
|
||||||
getDecoderInfos(mediaCodecSelector, inputFormat, drmSessionRequiresSecureDecoder);
|
getDecoderInfos(mediaCodecSelector, inputFormat, mediaCryptoRequiresSecureDecoder);
|
||||||
if (codecInfos.isEmpty() && drmSessionRequiresSecureDecoder) {
|
if (codecInfos.isEmpty() && mediaCryptoRequiresSecureDecoder) {
|
||||||
// The drm session indicates that a secure decoder is required, but the device does not
|
// The drm session indicates that a secure decoder is required, but the device does not
|
||||||
// have one. Assuming that supportsFormat indicated support for the media being played, we
|
// have one. Assuming that supportsFormat indicated support for the media being played, we
|
||||||
// know that it does not require a secure output path. Most CDM implementations allow
|
// know that it does not require a secure output path. Most CDM implementations allow
|
||||||
@ -928,6 +935,24 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setSourceDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) {
|
||||||
|
DrmSession<FrameworkMediaCrypto> previous = sourceDrmSession;
|
||||||
|
sourceDrmSession = session;
|
||||||
|
releaseDrmSessionIfUnused(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCodecDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) {
|
||||||
|
DrmSession<FrameworkMediaCrypto> previous = codecDrmSession;
|
||||||
|
codecDrmSession = session;
|
||||||
|
releaseDrmSessionIfUnused(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseDrmSessionIfUnused(@Nullable DrmSession<FrameworkMediaCrypto> session) {
|
||||||
|
if (session != null && session != sourceDrmSession && session != codecDrmSession) {
|
||||||
|
drmSessionManager.releaseSession(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Whether it may be possible to feed more input data.
|
* @return Whether it may be possible to feed more input data.
|
||||||
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
|
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
|
||||||
@ -1082,12 +1107,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||||
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
if (codecDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
@DrmSession.State int drmSessionState = codecDrmSession.getState();
|
||||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
|
||||||
}
|
}
|
||||||
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
||||||
}
|
}
|
||||||
@ -1126,13 +1151,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
throw ExoPlaybackException.createForRenderer(
|
throw ExoPlaybackException.createForRenderer(
|
||||||
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
||||||
}
|
}
|
||||||
pendingDrmSession =
|
DrmSession<FrameworkMediaCrypto> session =
|
||||||
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
|
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
|
||||||
if (pendingDrmSession == drmSession) {
|
if (session == sourceDrmSession || session == codecDrmSession) {
|
||||||
drmSessionManager.releaseSession(pendingDrmSession);
|
// We already had this session. The manager must be reference counting, so release it once
|
||||||
|
// to get the count attributed to this renderer back down to 1.
|
||||||
|
drmSessionManager.releaseSession(session);
|
||||||
}
|
}
|
||||||
|
setSourceDrmSession(session);
|
||||||
} else {
|
} else {
|
||||||
pendingDrmSession = null;
|
setSourceDrmSession(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1143,17 +1171,30 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
|
|
||||||
// We have an existing codec that we may need to reconfigure or re-initialize. If the existing
|
// We have an existing codec that we may need to reconfigure or re-initialize. If the existing
|
||||||
// codec instance is being kept then its operating rate may need to be updated.
|
// codec instance is being kept then its operating rate may need to be updated.
|
||||||
if (pendingDrmSession != drmSession) {
|
|
||||||
|
if ((sourceDrmSession == null && codecDrmSession != null)
|
||||||
|
|| (sourceDrmSession != null && codecDrmSession == null)
|
||||||
|
|| (sourceDrmSession != null && !codecInfo.secure)
|
||||||
|
|| (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) {
|
||||||
|
// We might need to switch between the clear and protected output paths, or we're using DRM
|
||||||
|
// prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM
|
||||||
|
// session.
|
||||||
drainAndReinitializeCodec();
|
drainAndReinitializeCodec();
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {
|
switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {
|
||||||
case KEEP_CODEC_RESULT_NO:
|
case KEEP_CODEC_RESULT_NO:
|
||||||
drainAndReinitializeCodec();
|
drainAndReinitializeCodec();
|
||||||
break;
|
break;
|
||||||
case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
|
case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
|
||||||
drainAndFlushCodec();
|
|
||||||
codecFormat = newFormat;
|
codecFormat = newFormat;
|
||||||
updateCodecOperatingRate();
|
updateCodecOperatingRate();
|
||||||
|
if (sourceDrmSession != codecDrmSession) {
|
||||||
|
drainAndUpdateCodecDrmSession();
|
||||||
|
} else {
|
||||||
|
drainAndFlushCodec();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION:
|
case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION:
|
||||||
if (codecNeedsReconfigureWorkaround) {
|
if (codecNeedsReconfigureWorkaround) {
|
||||||
@ -1168,17 +1209,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
&& newFormat.height == codecFormat.height);
|
&& newFormat.height == codecFormat.height);
|
||||||
codecFormat = newFormat;
|
codecFormat = newFormat;
|
||||||
updateCodecOperatingRate();
|
updateCodecOperatingRate();
|
||||||
|
if (sourceDrmSession != codecDrmSession) {
|
||||||
|
drainAndUpdateCodecDrmSession();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:
|
case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:
|
||||||
codecFormat = newFormat;
|
codecFormat = newFormat;
|
||||||
updateCodecOperatingRate();
|
updateCodecOperatingRate();
|
||||||
|
if (sourceDrmSession != codecDrmSession) {
|
||||||
|
drainAndUpdateCodecDrmSession();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException(); // Never happens.
|
throw new IllegalStateException(); // Never happens.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the output format of the {@link MediaCodec} changes.
|
* Called when the output format of the {@link MediaCodec} changes.
|
||||||
@ -1311,6 +1357,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts draining the codec to update its DRM session. The update may occur immediately if no
|
||||||
|
* buffers have been queued to the codec.
|
||||||
|
*
|
||||||
|
* @throws ExoPlaybackException If an error occurs updating the codec's DRM session.
|
||||||
|
*/
|
||||||
|
private void drainAndUpdateCodecDrmSession() throws ExoPlaybackException {
|
||||||
|
if (Util.SDK_INT < 23) {
|
||||||
|
// The codec needs to be re-initialized to switch to the source DRM session.
|
||||||
|
drainAndReinitializeCodec();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (codecReceivedBuffers) {
|
||||||
|
codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM;
|
||||||
|
codecDrainAction = DRAIN_ACTION_UPDATE_DRM_SESSION;
|
||||||
|
} else {
|
||||||
|
// Nothing has been queued to the decoder, so we can do the update immediately.
|
||||||
|
updateDrmSessionOrReinitializeCodecV23();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts draining the codec for re-initialization. Re-initialization may occur immediately if no
|
* Starts draining the codec for re-initialization. Re-initialization may occur immediately if no
|
||||||
* buffers have been queued to the codec.
|
* buffers have been queued to the codec.
|
||||||
@ -1323,8 +1390,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
codecDrainAction = DRAIN_ACTION_REINITIALIZE;
|
codecDrainAction = DRAIN_ACTION_REINITIALIZE;
|
||||||
} else {
|
} else {
|
||||||
// Nothing has been queued to the decoder, so we can re-initialize immediately.
|
// Nothing has been queued to the decoder, so we can re-initialize immediately.
|
||||||
releaseCodec();
|
reinitializeCodec();
|
||||||
maybeInitCodec();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1528,11 +1594,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
private void processEndOfStream() throws ExoPlaybackException {
|
private void processEndOfStream() throws ExoPlaybackException {
|
||||||
switch (codecDrainAction) {
|
switch (codecDrainAction) {
|
||||||
case DRAIN_ACTION_REINITIALIZE:
|
case DRAIN_ACTION_REINITIALIZE:
|
||||||
releaseCodec();
|
reinitializeCodec();
|
||||||
maybeInitCodec();
|
break;
|
||||||
|
case DRAIN_ACTION_UPDATE_DRM_SESSION:
|
||||||
|
updateDrmSessionOrReinitializeCodecV23();
|
||||||
break;
|
break;
|
||||||
case DRAIN_ACTION_FLUSH:
|
case DRAIN_ACTION_FLUSH:
|
||||||
flushOrReinitCodec();
|
flushOrReinitializeCodec();
|
||||||
break;
|
break;
|
||||||
case DRAIN_ACTION_NONE:
|
case DRAIN_ACTION_NONE:
|
||||||
default:
|
default:
|
||||||
@ -1542,6 +1610,41 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void reinitializeCodec() throws ExoPlaybackException {
|
||||||
|
releaseCodec();
|
||||||
|
maybeInitCodec();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(23)
|
||||||
|
private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackException {
|
||||||
|
FrameworkMediaCrypto sessionMediaCrypto = sourceDrmSession.getMediaCrypto();
|
||||||
|
if (sessionMediaCrypto == null) {
|
||||||
|
// We'd only expect this to happen if the CDM from which the pending session is obtained needs
|
||||||
|
// provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme
|
||||||
|
// to another, where the new CDM hasn't been used before and needs provisioning). It would be
|
||||||
|
// possible to handle this case more efficiently (i.e. with a new renderer state that waits
|
||||||
|
// for provisioning to finish and then calls mediaCrypto.setMediaDrmSession), but the extra
|
||||||
|
// complexity is not warranted given how unlikely the case is to occur.
|
||||||
|
reinitializeCodec();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flushOrReinitializeCodec()) {
|
||||||
|
// The codec was reinitialized. The new codec will be using the new DRM session, so there's
|
||||||
|
// nothing more to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId);
|
||||||
|
} catch (MediaCryptoException e) {
|
||||||
|
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||||
|
}
|
||||||
|
setCodecDrmSession(sourceDrmSession);
|
||||||
|
codecDrainState = DRAIN_STATE_NONE;
|
||||||
|
codecDrainAction = DRAIN_ACTION_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean shouldSkipOutputBuffer(long presentationTimeUs) {
|
private boolean shouldSkipOutputBuffer(long presentationTimeUs) {
|
||||||
// We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would
|
// We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would
|
||||||
// box presentationTimeUs, creating a Long object that would need to be garbage collected.
|
// box presentationTimeUs, creating a Long object that would need to be garbage collected.
|
||||||
@ -1557,7 +1660,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
|
|
||||||
private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(
|
private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(
|
||||||
DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) {
|
DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) {
|
||||||
MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16();
|
MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfo();
|
||||||
if (adaptiveReconfigurationBytes == 0) {
|
if (adaptiveReconfigurationBytes == 0) {
|
||||||
return cryptoInfo;
|
return cryptoInfo;
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,6 @@ import java.util.regex.Pattern;
|
|||||||
/**
|
/**
|
||||||
* A utility class for querying the available codecs.
|
* A utility class for querying the available codecs.
|
||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
public final class MediaCodecUtil {
|
public final class MediaCodecUtil {
|
||||||
|
|
||||||
@ -59,8 +58,6 @@ public final class MediaCodecUtil {
|
|||||||
|
|
||||||
private static final String TAG = "MediaCodecUtil";
|
private static final String TAG = "MediaCodecUtil";
|
||||||
private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$");
|
private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$");
|
||||||
private static final RawAudioCodecComparator RAW_AUDIO_CODEC_COMPARATOR =
|
|
||||||
new RawAudioCodecComparator();
|
|
||||||
|
|
||||||
private static final HashMap<CodecKey, List<MediaCodecInfo>> decoderInfosCache = new HashMap<>();
|
private static final HashMap<CodecKey, List<MediaCodecInfo>> decoderInfosCache = new HashMap<>();
|
||||||
|
|
||||||
@ -312,30 +309,6 @@ public final class MediaCodecUtil {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Work around https://github.com/google/ExoPlayer/issues/398.
|
|
||||||
if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Work around https://github.com/google/ExoPlayer/issues/4519.
|
|
||||||
if ("OMX.SEC.mp3.dec".equals(name)
|
|
||||||
&& (Util.MODEL.startsWith("GT-I9152")
|
|
||||||
|| Util.MODEL.startsWith("GT-I9515")
|
|
||||||
|| Util.MODEL.startsWith("GT-P5220")
|
|
||||||
|| Util.MODEL.startsWith("GT-S7580")
|
|
||||||
|| Util.MODEL.startsWith("SM-G350")
|
|
||||||
|| Util.MODEL.startsWith("SM-G386")
|
|
||||||
|| Util.MODEL.startsWith("SM-T231")
|
|
||||||
|| Util.MODEL.startsWith("SM-T530"))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ("OMX.brcm.audio.mp3.decoder".equals(name)
|
|
||||||
&& (Util.MODEL.startsWith("GT-I9152")
|
|
||||||
|| Util.MODEL.startsWith("GT-S7580")
|
|
||||||
|| Util.MODEL.startsWith("SM-G350"))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Work around https://github.com/google/ExoPlayer/issues/1528 and
|
// Work around https://github.com/google/ExoPlayer/issues/1528 and
|
||||||
// https://github.com/google/ExoPlayer/issues/3171.
|
// https://github.com/google/ExoPlayer/issues/3171.
|
||||||
if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name)
|
if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name)
|
||||||
@ -422,7 +395,18 @@ public final class MediaCodecUtil {
|
|||||||
*/
|
*/
|
||||||
private static void applyWorkarounds(String mimeType, List<MediaCodecInfo> decoderInfos) {
|
private static void applyWorkarounds(String mimeType, List<MediaCodecInfo> decoderInfos) {
|
||||||
if (MimeTypes.AUDIO_RAW.equals(mimeType)) {
|
if (MimeTypes.AUDIO_RAW.equals(mimeType)) {
|
||||||
Collections.sort(decoderInfos, RAW_AUDIO_CODEC_COMPARATOR);
|
Collections.sort(decoderInfos, new RawAudioCodecComparator());
|
||||||
|
} else if (Util.SDK_INT < 21 && decoderInfos.size() > 1) {
|
||||||
|
String firstCodecName = decoderInfos.get(0).name;
|
||||||
|
if ("OMX.SEC.mp3.dec".equals(firstCodecName)
|
||||||
|
|| "OMX.SEC.MP3.Decoder".equals(firstCodecName)
|
||||||
|
|| "OMX.brcm.audio.mp3.decoder".equals(firstCodecName)) {
|
||||||
|
// Prefer OMX.google codecs over OMX.SEC.mp3.dec, OMX.SEC.MP3.Decoder and
|
||||||
|
// OMX.brcm.audio.mp3.decoder on older devices. See:
|
||||||
|
// https://github.com/google/ExoPlayer/issues/398 and
|
||||||
|
// https://github.com/google/ExoPlayer/issues/4519.
|
||||||
|
Collections.sort(decoderInfos, new PreferOmxGoogleCodecComparator());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,9 +445,10 @@ public final class MediaCodecUtil {
|
|||||||
Log.w(TAG, "Unknown HEVC profile string: " + profileString);
|
Log.w(TAG, "Unknown HEVC profile string: " + profileString);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(parts[3]);
|
String levelString = parts[3];
|
||||||
|
Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(levelString);
|
||||||
if (level == null) {
|
if (level == null) {
|
||||||
Log.w(TAG, "Unknown HEVC level string: " + matcher.group(1));
|
Log.w(TAG, "Unknown HEVC level string: " + levelString);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new Pair<>(profile, level);
|
return new Pair<>(profile, level);
|
||||||
@ -728,6 +713,18 @@ public final class MediaCodecUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Comparator for preferring OMX.google media codecs. */
|
||||||
|
private static final class PreferOmxGoogleCodecComparator implements Comparator<MediaCodecInfo> {
|
||||||
|
@Override
|
||||||
|
public int compare(MediaCodecInfo a, MediaCodecInfo b) {
|
||||||
|
return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) {
|
||||||
|
return mediaCodecInfo.name.startsWith("OMX.google") ? -1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray();
|
AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray();
|
||||||
AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline);
|
AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline);
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.mediacodec;
|
package com.google.android.exoplayer2.mediacodec;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
@ -24,7 +23,6 @@ import java.nio.ByteBuffer;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Helper class for configuring {@link MediaFormat} instances. */
|
/** Helper class for configuring {@link MediaFormat} instances. */
|
||||||
@TargetApi(16)
|
|
||||||
public final class MediaFormatUtil {
|
public final class MediaFormatUtil {
|
||||||
|
|
||||||
private MediaFormatUtil() {}
|
private MediaFormatUtil() {}
|
||||||
|
@ -0,0 +1,350 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.offline;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.VisibleForTesting;
|
||||||
|
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||||
|
import com.google.android.exoplayer2.database.VersionTable;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link DownloadIndex} which uses SQLite to persist {@link DownloadState}s.
|
||||||
|
*
|
||||||
|
* <p class="caution">Database access may take a long time, do not call methods of this class from
|
||||||
|
* the application main thread.
|
||||||
|
*/
|
||||||
|
public final class DefaultDownloadIndex implements DownloadIndex {
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
/* package */ static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Downloads";
|
||||||
|
|
||||||
|
@VisibleForTesting /* package */ static final int TABLE_VERSION = 1;
|
||||||
|
|
||||||
|
private static final String COLUMN_ID = "id";
|
||||||
|
private static final String COLUMN_TYPE = "title";
|
||||||
|
private static final String COLUMN_URI = "subtitle";
|
||||||
|
private static final String COLUMN_CACHE_KEY = "cache_key";
|
||||||
|
private static final String COLUMN_STATE = "state";
|
||||||
|
private static final String COLUMN_DOWNLOAD_PERCENTAGE = "download_percentage";
|
||||||
|
private static final String COLUMN_DOWNLOADED_BYTES = "downloaded_bytes";
|
||||||
|
private static final String COLUMN_TOTAL_BYTES = "total_bytes";
|
||||||
|
private static final String COLUMN_FAILURE_REASON = "failure_reason";
|
||||||
|
private static final String COLUMN_STOP_FLAGS = "stop_flags";
|
||||||
|
private static final String COLUMN_NOT_MET_REQUIREMENTS = "not_met_requirements";
|
||||||
|
private static final String COLUMN_MANUAL_STOP_REASON = "manual_stop_reason";
|
||||||
|
private static final String COLUMN_START_TIME_MS = "start_time_ms";
|
||||||
|
private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms";
|
||||||
|
private static final String COLUMN_STREAM_KEYS = "stream_keys";
|
||||||
|
private static final String COLUMN_CUSTOM_METADATA = "custom_metadata";
|
||||||
|
|
||||||
|
private static final int COLUMN_INDEX_ID = 0;
|
||||||
|
private static final int COLUMN_INDEX_TYPE = 1;
|
||||||
|
private static final int COLUMN_INDEX_URI = 2;
|
||||||
|
private static final int COLUMN_INDEX_CACHE_KEY = 3;
|
||||||
|
private static final int COLUMN_INDEX_STATE = 4;
|
||||||
|
private static final int COLUMN_INDEX_DOWNLOAD_PERCENTAGE = 5;
|
||||||
|
private static final int COLUMN_INDEX_DOWNLOADED_BYTES = 6;
|
||||||
|
private static final int COLUMN_INDEX_TOTAL_BYTES = 7;
|
||||||
|
private static final int COLUMN_INDEX_FAILURE_REASON = 8;
|
||||||
|
private static final int COLUMN_INDEX_STOP_FLAGS = 9;
|
||||||
|
private static final int COLUMN_INDEX_NOT_MET_REQUIREMENTS = 10;
|
||||||
|
private static final int COLUMN_INDEX_MANUAL_STOP_REASON = 11;
|
||||||
|
private static final int COLUMN_INDEX_START_TIME_MS = 12;
|
||||||
|
private static final int COLUMN_INDEX_UPDATE_TIME_MS = 13;
|
||||||
|
private static final int COLUMN_INDEX_STREAM_KEYS = 14;
|
||||||
|
private static final int COLUMN_INDEX_CUSTOM_METADATA = 15;
|
||||||
|
|
||||||
|
private static final String COLUMN_SELECTION_ID = COLUMN_ID + " = ?";
|
||||||
|
|
||||||
|
private static final String[] COLUMNS =
|
||||||
|
new String[] {
|
||||||
|
COLUMN_ID,
|
||||||
|
COLUMN_TYPE,
|
||||||
|
COLUMN_URI,
|
||||||
|
COLUMN_CACHE_KEY,
|
||||||
|
COLUMN_STATE,
|
||||||
|
COLUMN_DOWNLOAD_PERCENTAGE,
|
||||||
|
COLUMN_DOWNLOADED_BYTES,
|
||||||
|
COLUMN_TOTAL_BYTES,
|
||||||
|
COLUMN_FAILURE_REASON,
|
||||||
|
COLUMN_STOP_FLAGS,
|
||||||
|
COLUMN_NOT_MET_REQUIREMENTS,
|
||||||
|
COLUMN_MANUAL_STOP_REASON,
|
||||||
|
COLUMN_START_TIME_MS,
|
||||||
|
COLUMN_UPDATE_TIME_MS,
|
||||||
|
COLUMN_STREAM_KEYS,
|
||||||
|
COLUMN_CUSTOM_METADATA
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final String SQL_DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||||
|
private static final String SQL_CREATE_TABLE =
|
||||||
|
"CREATE TABLE "
|
||||||
|
+ TABLE_NAME
|
||||||
|
+ " ("
|
||||||
|
+ COLUMN_ID
|
||||||
|
+ " TEXT PRIMARY KEY NOT NULL,"
|
||||||
|
+ COLUMN_TYPE
|
||||||
|
+ " TEXT NOT NULL,"
|
||||||
|
+ COLUMN_URI
|
||||||
|
+ " TEXT NOT NULL,"
|
||||||
|
+ COLUMN_CACHE_KEY
|
||||||
|
+ " TEXT,"
|
||||||
|
+ COLUMN_STATE
|
||||||
|
+ " INTEGER NOT NULL,"
|
||||||
|
+ COLUMN_DOWNLOAD_PERCENTAGE
|
||||||
|
+ " REAL NOT NULL,"
|
||||||
|
+ COLUMN_DOWNLOADED_BYTES
|
||||||
|
+ " INTEGER NOT NULL,"
|
||||||
|
+ COLUMN_TOTAL_BYTES
|
||||||
|
+ " INTEGER NOT NULL,"
|
||||||
|
+ COLUMN_FAILURE_REASON
|
||||||
|
+ " INTEGER NOT NULL,"
|
||||||
|
+ COLUMN_STOP_FLAGS
|
||||||
|
+ " INTEGER NOT NULL,"
|
||||||
|
+ COLUMN_NOT_MET_REQUIREMENTS
|
||||||
|
+ " INTEGER NOT NULL,"
|
||||||
|
+ COLUMN_MANUAL_STOP_REASON
|
||||||
|
+ " INTEGER NOT NULL,"
|
||||||
|
+ COLUMN_START_TIME_MS
|
||||||
|
+ " INTEGER NOT NULL,"
|
||||||
|
+ COLUMN_UPDATE_TIME_MS
|
||||||
|
+ " INTEGER NOT NULL,"
|
||||||
|
+ COLUMN_STREAM_KEYS
|
||||||
|
+ " TEXT NOT NULL,"
|
||||||
|
+ COLUMN_CUSTOM_METADATA
|
||||||
|
+ " BLOB NOT NULL)";
|
||||||
|
|
||||||
|
private final DatabaseProvider databaseProvider;
|
||||||
|
|
||||||
|
private boolean initialized;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a DefaultDownloadIndex which stores the {@link DownloadState}s on a SQLite database
|
||||||
|
* provided by {@code databaseProvider}.
|
||||||
|
*
|
||||||
|
* @param databaseProvider A DatabaseProvider which provides the database which will be used to
|
||||||
|
* store DownloadStatus table.
|
||||||
|
*/
|
||||||
|
public DefaultDownloadIndex(DatabaseProvider databaseProvider) {
|
||||||
|
this.databaseProvider = databaseProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public DownloadState getDownloadState(String id) {
|
||||||
|
ensureInitialized();
|
||||||
|
try (Cursor cursor = getCursor(COLUMN_SELECTION_ID, new String[] {id})) {
|
||||||
|
if (cursor.getCount() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
cursor.moveToNext();
|
||||||
|
DownloadState downloadState = getDownloadStateForCurrentRow(cursor);
|
||||||
|
Assertions.checkState(id.equals(downloadState.id));
|
||||||
|
return downloadState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DownloadStateCursor getDownloadStates(@DownloadState.State int... states) {
|
||||||
|
ensureInitialized();
|
||||||
|
String selection = null;
|
||||||
|
if (states.length > 0) {
|
||||||
|
StringBuilder selectionBuilder = new StringBuilder();
|
||||||
|
selectionBuilder.append(COLUMN_STATE).append(" IN (");
|
||||||
|
for (int i = 0; i < states.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
selectionBuilder.append(',');
|
||||||
|
}
|
||||||
|
selectionBuilder.append(states[i]);
|
||||||
|
}
|
||||||
|
selectionBuilder.append(')');
|
||||||
|
selection = selectionBuilder.toString();
|
||||||
|
}
|
||||||
|
Cursor cursor = getCursor(selection, /* selectionArgs= */ null);
|
||||||
|
return new DownloadStateCursorImpl(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putDownloadState(DownloadState downloadState) {
|
||||||
|
ensureInitialized();
|
||||||
|
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(COLUMN_ID, downloadState.id);
|
||||||
|
values.put(COLUMN_TYPE, downloadState.type);
|
||||||
|
values.put(COLUMN_URI, downloadState.uri.toString());
|
||||||
|
values.put(COLUMN_CACHE_KEY, downloadState.cacheKey);
|
||||||
|
values.put(COLUMN_STATE, downloadState.state);
|
||||||
|
values.put(COLUMN_DOWNLOAD_PERCENTAGE, downloadState.downloadPercentage);
|
||||||
|
values.put(COLUMN_DOWNLOADED_BYTES, downloadState.downloadedBytes);
|
||||||
|
values.put(COLUMN_TOTAL_BYTES, downloadState.totalBytes);
|
||||||
|
values.put(COLUMN_FAILURE_REASON, downloadState.failureReason);
|
||||||
|
values.put(COLUMN_STOP_FLAGS, downloadState.stopFlags);
|
||||||
|
values.put(COLUMN_NOT_MET_REQUIREMENTS, downloadState.notMetRequirements);
|
||||||
|
values.put(COLUMN_MANUAL_STOP_REASON, downloadState.manualStopReason);
|
||||||
|
values.put(COLUMN_START_TIME_MS, downloadState.startTimeMs);
|
||||||
|
values.put(COLUMN_UPDATE_TIME_MS, downloadState.updateTimeMs);
|
||||||
|
values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(downloadState.streamKeys));
|
||||||
|
values.put(COLUMN_CUSTOM_METADATA, downloadState.customMetadata);
|
||||||
|
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeDownloadState(String id) {
|
||||||
|
ensureInitialized();
|
||||||
|
databaseProvider
|
||||||
|
.getWritableDatabase()
|
||||||
|
.delete(TABLE_NAME, COLUMN_SELECTION_ID, new String[] {id});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureInitialized() {
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
|
||||||
|
int version = VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE);
|
||||||
|
if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) {
|
||||||
|
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
|
||||||
|
writableDatabase.beginTransaction();
|
||||||
|
try {
|
||||||
|
VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_OFFLINE, TABLE_VERSION);
|
||||||
|
writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS);
|
||||||
|
writableDatabase.execSQL(SQL_CREATE_TABLE);
|
||||||
|
writableDatabase.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
writableDatabase.endTransaction();
|
||||||
|
}
|
||||||
|
} else if (version < TABLE_VERSION) {
|
||||||
|
// There is no previous version currently.
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cursor getCursor(@Nullable String selection, @Nullable String[] selectionArgs) {
|
||||||
|
String sortOrder = COLUMN_START_TIME_MS + " ASC";
|
||||||
|
return databaseProvider
|
||||||
|
.getReadableDatabase()
|
||||||
|
.query(
|
||||||
|
TABLE_NAME,
|
||||||
|
COLUMNS,
|
||||||
|
selection,
|
||||||
|
selectionArgs,
|
||||||
|
/* groupBy= */ null,
|
||||||
|
/* having= */ null,
|
||||||
|
sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DownloadState getDownloadStateForCurrentRow(Cursor cursor) {
|
||||||
|
return new DownloadState(
|
||||||
|
cursor.getString(COLUMN_INDEX_ID),
|
||||||
|
cursor.getString(COLUMN_INDEX_TYPE),
|
||||||
|
Uri.parse(cursor.getString(COLUMN_INDEX_URI)),
|
||||||
|
cursor.getString(COLUMN_INDEX_CACHE_KEY),
|
||||||
|
cursor.getInt(COLUMN_INDEX_STATE),
|
||||||
|
cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE),
|
||||||
|
cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES),
|
||||||
|
cursor.getLong(COLUMN_INDEX_TOTAL_BYTES),
|
||||||
|
cursor.getInt(COLUMN_INDEX_FAILURE_REASON),
|
||||||
|
cursor.getInt(COLUMN_INDEX_STOP_FLAGS),
|
||||||
|
cursor.getInt(COLUMN_INDEX_NOT_MET_REQUIREMENTS),
|
||||||
|
cursor.getInt(COLUMN_INDEX_MANUAL_STOP_REASON),
|
||||||
|
cursor.getLong(COLUMN_INDEX_START_TIME_MS),
|
||||||
|
cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS),
|
||||||
|
decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)),
|
||||||
|
cursor.getBlob(COLUMN_INDEX_CUSTOM_METADATA));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String encodeStreamKeys(StreamKey[] streamKeys) {
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
for (StreamKey streamKey : streamKeys) {
|
||||||
|
stringBuilder
|
||||||
|
.append(streamKey.periodIndex)
|
||||||
|
.append('.')
|
||||||
|
.append(streamKey.groupIndex)
|
||||||
|
.append('.')
|
||||||
|
.append(streamKey.trackIndex)
|
||||||
|
.append(',');
|
||||||
|
}
|
||||||
|
if (stringBuilder.length() > 0) {
|
||||||
|
stringBuilder.setLength(stringBuilder.length() - 1);
|
||||||
|
}
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StreamKey[] decodeStreamKeys(String encodedStreamKeys) {
|
||||||
|
if (encodedStreamKeys.isEmpty()) {
|
||||||
|
return new StreamKey[0];
|
||||||
|
}
|
||||||
|
String[] streamKeysStrings = Util.split(encodedStreamKeys, ",");
|
||||||
|
int streamKeysCount = streamKeysStrings.length;
|
||||||
|
StreamKey[] streamKeys = new StreamKey[streamKeysCount];
|
||||||
|
for (int i = 0; i < streamKeysCount; i++) {
|
||||||
|
String[] indices = Util.split(streamKeysStrings[i], "\\.");
|
||||||
|
Assertions.checkState(indices.length == 3);
|
||||||
|
streamKeys[i] =
|
||||||
|
new StreamKey(
|
||||||
|
Integer.parseInt(indices[0]),
|
||||||
|
Integer.parseInt(indices[1]),
|
||||||
|
Integer.parseInt(indices[2]));
|
||||||
|
}
|
||||||
|
return streamKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DownloadStateCursorImpl implements DownloadStateCursor {
|
||||||
|
|
||||||
|
private final Cursor cursor;
|
||||||
|
|
||||||
|
private DownloadStateCursorImpl(Cursor cursor) {
|
||||||
|
this.cursor = cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DownloadState getDownloadState() {
|
||||||
|
return getDownloadStateForCurrentRow(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return cursor.getCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPosition() {
|
||||||
|
return cursor.getPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean moveToPosition(int position) {
|
||||||
|
return cursor.moveToPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isClosed() {
|
||||||
|
return cursor.isClosed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -156,7 +156,7 @@ public final class DownloadAction {
|
|||||||
ArrayList<StreamKey> mutableKeys = new ArrayList<>(keys);
|
ArrayList<StreamKey> mutableKeys = new ArrayList<>(keys);
|
||||||
Collections.sort(mutableKeys);
|
Collections.sort(mutableKeys);
|
||||||
this.keys = Collections.unmodifiableList(mutableKeys);
|
this.keys = Collections.unmodifiableList(mutableKeys);
|
||||||
this.data = data != null ? data : Util.EMPTY_BYTE_ARRAY;
|
this.data = data != null ? Arrays.copyOf(data, data.length) : Util.EMPTY_BYTE_ARRAY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,8 +17,11 @@ package com.google.android.exoplayer2.offline;
|
|||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.util.Pair;
|
||||||
import android.util.SparseIntArray;
|
import android.util.SparseIntArray;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
@ -27,6 +30,8 @@ import com.google.android.exoplayer2.RenderersFactory;
|
|||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||||
|
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
@ -36,11 +41,16 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Paramet
|
|||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||||
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -58,19 +68,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||||||
* <p>A typical usage of DownloadHelper follows these steps:
|
* <p>A typical usage of DownloadHelper follows these steps:
|
||||||
*
|
*
|
||||||
* <ol>
|
* <ol>
|
||||||
* <li>Construct the download helper with information about the {@link RenderersFactory renderers}
|
* <li>Build the helper using one of the {@code forXXX} methods.
|
||||||
* and {@link DefaultTrackSelector.Parameters parameters} for track selection.
|
|
||||||
* <li>Prepare the helper using {@link #prepare(Callback)} and wait for the callback.
|
* <li>Prepare the helper using {@link #prepare(Callback)} and wait for the callback.
|
||||||
* <li>Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link
|
* <li>Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link
|
||||||
* #getTrackSelections(int, int)}, and make adjustments using {@link
|
* #getTrackSelections(int, int)}, and make adjustments using {@link
|
||||||
* #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link
|
* #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link
|
||||||
* #addTrackSelection(int, Parameters)}.
|
* #addTrackSelection(int, Parameters)}.
|
||||||
* <li>Create download actions for the selected track using {@link #getDownloadAction(byte[])}.
|
* <li>Create a download action for the selected track using {@link #getDownloadAction(byte[])}.
|
||||||
|
* <li>Release the helper using {@link #release()}.
|
||||||
* </ol>
|
* </ol>
|
||||||
*
|
|
||||||
* @param <T> The manifest type.
|
|
||||||
*/
|
*/
|
||||||
public abstract class DownloadHelper<T> {
|
public final class DownloadHelper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default parameters used for track selection for downloading. This default selects the
|
* The default parameters used for track selection for downloading. This default selects the
|
||||||
@ -87,7 +95,7 @@ public abstract class DownloadHelper<T> {
|
|||||||
*
|
*
|
||||||
* @param helper The reporting {@link DownloadHelper}.
|
* @param helper The reporting {@link DownloadHelper}.
|
||||||
*/
|
*/
|
||||||
void onPrepared(DownloadHelper<?> helper);
|
void onPrepared(DownloadHelper helper);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when preparation fails.
|
* Called when preparation fails.
|
||||||
@ -95,18 +103,222 @@ public abstract class DownloadHelper<T> {
|
|||||||
* @param helper The reporting {@link DownloadHelper}.
|
* @param helper The reporting {@link DownloadHelper}.
|
||||||
* @param e The error.
|
* @param e The error.
|
||||||
*/
|
*/
|
||||||
void onPrepareError(DownloadHelper<?> helper, IOException e);
|
void onPrepareError(DownloadHelper helper, IOException e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable private static final Constructor<?> DASH_FACTORY_CONSTRUCTOR;
|
||||||
|
@Nullable private static final Constructor<?> HLS_FACTORY_CONSTRUCTOR;
|
||||||
|
@Nullable private static final Constructor<?> SS_FACTORY_CONSTRUCTOR;
|
||||||
|
@Nullable private static final Method DASH_FACTORY_CREATE_METHOD;
|
||||||
|
@Nullable private static final Method HLS_FACTORY_CREATE_METHOD;
|
||||||
|
@Nullable private static final Method SS_FACTORY_CREATE_METHOD;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Pair<@NullableType Constructor<?>, @NullableType Method> dashFactoryMethods =
|
||||||
|
getMediaSourceFactoryMethods(
|
||||||
|
"com.google.android.exoplayer2.source.dash.DashMediaSource$Factory");
|
||||||
|
DASH_FACTORY_CONSTRUCTOR = dashFactoryMethods.first;
|
||||||
|
DASH_FACTORY_CREATE_METHOD = dashFactoryMethods.second;
|
||||||
|
Pair<@NullableType Constructor<?>, @NullableType Method> hlsFactoryMethods =
|
||||||
|
getMediaSourceFactoryMethods(
|
||||||
|
"com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory");
|
||||||
|
HLS_FACTORY_CONSTRUCTOR = hlsFactoryMethods.first;
|
||||||
|
HLS_FACTORY_CREATE_METHOD = hlsFactoryMethods.second;
|
||||||
|
Pair<@NullableType Constructor<?>, @NullableType Method> ssFactoryMethods =
|
||||||
|
getMediaSourceFactoryMethods(
|
||||||
|
"com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory");
|
||||||
|
SS_FACTORY_CONSTRUCTOR = ssFactoryMethods.first;
|
||||||
|
SS_FACTORY_CREATE_METHOD = ssFactoryMethods.second;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DownloadHelper} for progressive streams.
|
||||||
|
*
|
||||||
|
* @param uri A stream {@link Uri}.
|
||||||
|
* @return A {@link DownloadHelper} for progressive streams.
|
||||||
|
*/
|
||||||
|
public static DownloadHelper forProgressive(Uri uri) {
|
||||||
|
return forProgressive(uri, /* cacheKey= */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DownloadHelper} for progressive streams.
|
||||||
|
*
|
||||||
|
* @param uri A stream {@link Uri}.
|
||||||
|
* @param cacheKey An optional cache key.
|
||||||
|
* @return A {@link DownloadHelper} for progressive streams.
|
||||||
|
*/
|
||||||
|
public static DownloadHelper forProgressive(Uri uri, @Nullable String cacheKey) {
|
||||||
|
return new DownloadHelper(
|
||||||
|
DownloadAction.TYPE_PROGRESSIVE,
|
||||||
|
uri,
|
||||||
|
cacheKey,
|
||||||
|
/* mediaSource= */ null,
|
||||||
|
DEFAULT_TRACK_SELECTOR_PARAMETERS,
|
||||||
|
/* rendererCapabilities= */ new RendererCapabilities[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DownloadHelper} for DASH streams.
|
||||||
|
*
|
||||||
|
* @param uri A manifest {@link Uri}.
|
||||||
|
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.
|
||||||
|
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
|
||||||
|
* selected.
|
||||||
|
* @return A {@link DownloadHelper} for DASH streams.
|
||||||
|
* @throws IllegalStateException If the DASH module is missing.
|
||||||
|
*/
|
||||||
|
public static DownloadHelper forDash(
|
||||||
|
Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {
|
||||||
|
return forDash(
|
||||||
|
uri,
|
||||||
|
dataSourceFactory,
|
||||||
|
renderersFactory,
|
||||||
|
/* drmSessionManager= */ null,
|
||||||
|
DEFAULT_TRACK_SELECTOR_PARAMETERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DownloadHelper} for DASH streams.
|
||||||
|
*
|
||||||
|
* @param uri A manifest {@link Uri}.
|
||||||
|
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.
|
||||||
|
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
|
||||||
|
* selected.
|
||||||
|
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
|
||||||
|
* {@code renderersFactory}.
|
||||||
|
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
|
||||||
|
* downloading.
|
||||||
|
* @return A {@link DownloadHelper} for DASH streams.
|
||||||
|
* @throws IllegalStateException If the DASH module is missing.
|
||||||
|
*/
|
||||||
|
public static DownloadHelper forDash(
|
||||||
|
Uri uri,
|
||||||
|
DataSource.Factory dataSourceFactory,
|
||||||
|
RenderersFactory renderersFactory,
|
||||||
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
|
DefaultTrackSelector.Parameters trackSelectorParameters) {
|
||||||
|
return new DownloadHelper(
|
||||||
|
DownloadAction.TYPE_DASH,
|
||||||
|
uri,
|
||||||
|
/* cacheKey= */ null,
|
||||||
|
createMediaSource(
|
||||||
|
uri, dataSourceFactory, DASH_FACTORY_CONSTRUCTOR, DASH_FACTORY_CREATE_METHOD),
|
||||||
|
trackSelectorParameters,
|
||||||
|
Util.getRendererCapabilities(renderersFactory, drmSessionManager));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DownloadHelper} for HLS streams.
|
||||||
|
*
|
||||||
|
* @param uri A playlist {@link Uri}.
|
||||||
|
* @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist.
|
||||||
|
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
|
||||||
|
* selected.
|
||||||
|
* @return A {@link DownloadHelper} for HLS streams.
|
||||||
|
* @throws IllegalStateException If the HLS module is missing.
|
||||||
|
*/
|
||||||
|
public static DownloadHelper forHls(
|
||||||
|
Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {
|
||||||
|
return forHls(
|
||||||
|
uri,
|
||||||
|
dataSourceFactory,
|
||||||
|
renderersFactory,
|
||||||
|
/* drmSessionManager= */ null,
|
||||||
|
DEFAULT_TRACK_SELECTOR_PARAMETERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DownloadHelper} for HLS streams.
|
||||||
|
*
|
||||||
|
* @param uri A playlist {@link Uri}.
|
||||||
|
* @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist.
|
||||||
|
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
|
||||||
|
* selected.
|
||||||
|
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
|
||||||
|
* {@code renderersFactory}.
|
||||||
|
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
|
||||||
|
* downloading.
|
||||||
|
* @return A {@link DownloadHelper} for HLS streams.
|
||||||
|
* @throws IllegalStateException If the HLS module is missing.
|
||||||
|
*/
|
||||||
|
public static DownloadHelper forHls(
|
||||||
|
Uri uri,
|
||||||
|
DataSource.Factory dataSourceFactory,
|
||||||
|
RenderersFactory renderersFactory,
|
||||||
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
|
DefaultTrackSelector.Parameters trackSelectorParameters) {
|
||||||
|
return new DownloadHelper(
|
||||||
|
DownloadAction.TYPE_HLS,
|
||||||
|
uri,
|
||||||
|
/* cacheKey= */ null,
|
||||||
|
createMediaSource(
|
||||||
|
uri, dataSourceFactory, HLS_FACTORY_CONSTRUCTOR, HLS_FACTORY_CREATE_METHOD),
|
||||||
|
trackSelectorParameters,
|
||||||
|
Util.getRendererCapabilities(renderersFactory, drmSessionManager));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DownloadHelper} for SmoothStreaming streams.
|
||||||
|
*
|
||||||
|
* @param uri A manifest {@link Uri}.
|
||||||
|
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.
|
||||||
|
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
|
||||||
|
* selected.
|
||||||
|
* @return A {@link DownloadHelper} for SmoothStreaming streams.
|
||||||
|
* @throws IllegalStateException If the SmoothStreaming module is missing.
|
||||||
|
*/
|
||||||
|
public static DownloadHelper forSmoothStreaming(
|
||||||
|
Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {
|
||||||
|
return forSmoothStreaming(
|
||||||
|
uri,
|
||||||
|
dataSourceFactory,
|
||||||
|
renderersFactory,
|
||||||
|
/* drmSessionManager= */ null,
|
||||||
|
DEFAULT_TRACK_SELECTOR_PARAMETERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DownloadHelper} for SmoothStreaming streams.
|
||||||
|
*
|
||||||
|
* @param uri A manifest {@link Uri}.
|
||||||
|
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.
|
||||||
|
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
|
||||||
|
* selected.
|
||||||
|
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
|
||||||
|
* {@code renderersFactory}.
|
||||||
|
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
|
||||||
|
* downloading.
|
||||||
|
* @return A {@link DownloadHelper} for SmoothStreaming streams.
|
||||||
|
* @throws IllegalStateException If the SmoothStreaming module is missing.
|
||||||
|
*/
|
||||||
|
public static DownloadHelper forSmoothStreaming(
|
||||||
|
Uri uri,
|
||||||
|
DataSource.Factory dataSourceFactory,
|
||||||
|
RenderersFactory renderersFactory,
|
||||||
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
|
DefaultTrackSelector.Parameters trackSelectorParameters) {
|
||||||
|
return new DownloadHelper(
|
||||||
|
DownloadAction.TYPE_SS,
|
||||||
|
uri,
|
||||||
|
/* cacheKey= */ null,
|
||||||
|
createMediaSource(uri, dataSourceFactory, SS_FACTORY_CONSTRUCTOR, SS_FACTORY_CREATE_METHOD),
|
||||||
|
trackSelectorParameters,
|
||||||
|
Util.getRendererCapabilities(renderersFactory, drmSessionManager));
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String downloadType;
|
private final String downloadType;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
@Nullable private final String cacheKey;
|
@Nullable private final String cacheKey;
|
||||||
|
@Nullable private final MediaSource mediaSource;
|
||||||
private final DefaultTrackSelector trackSelector;
|
private final DefaultTrackSelector trackSelector;
|
||||||
private final RendererCapabilities[] rendererCapabilities;
|
private final RendererCapabilities[] rendererCapabilities;
|
||||||
private final SparseIntArray scratchSet;
|
private final SparseIntArray scratchSet;
|
||||||
|
|
||||||
private int currentTrackSelectionPeriodIndex;
|
private boolean isPreparedWithMedia;
|
||||||
@Nullable private T manifest;
|
private @MonotonicNonNull Callback callback;
|
||||||
|
private @MonotonicNonNull Handler callbackHandler;
|
||||||
|
private @MonotonicNonNull MediaPreparer mediaPreparer;
|
||||||
private TrackGroupArray @MonotonicNonNull [] trackGroupArrays;
|
private TrackGroupArray @MonotonicNonNull [] trackGroupArrays;
|
||||||
private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos;
|
private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos;
|
||||||
private List<TrackSelection> @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer;
|
private List<TrackSelection> @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer;
|
||||||
@ -118,25 +330,26 @@ public abstract class DownloadHelper<T> {
|
|||||||
* @param downloadType A download type. This value will be used as {@link DownloadAction#type}.
|
* @param downloadType A download type. This value will be used as {@link DownloadAction#type}.
|
||||||
* @param uri A {@link Uri}.
|
* @param uri A {@link Uri}.
|
||||||
* @param cacheKey An optional cache key.
|
* @param cacheKey An optional cache key.
|
||||||
|
* @param mediaSource A {@link MediaSource} for which tracks are selected, or null if no track
|
||||||
|
* selection needs to be made.
|
||||||
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
|
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
|
||||||
* downloading.
|
* downloading.
|
||||||
* @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
|
* @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks
|
||||||
* are selected.
|
* are selected.
|
||||||
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
|
|
||||||
* {@code renderersFactory}.
|
|
||||||
*/
|
*/
|
||||||
public DownloadHelper(
|
public DownloadHelper(
|
||||||
String downloadType,
|
String downloadType,
|
||||||
Uri uri,
|
Uri uri,
|
||||||
@Nullable String cacheKey,
|
@Nullable String cacheKey,
|
||||||
|
@Nullable MediaSource mediaSource,
|
||||||
DefaultTrackSelector.Parameters trackSelectorParameters,
|
DefaultTrackSelector.Parameters trackSelectorParameters,
|
||||||
RenderersFactory renderersFactory,
|
RendererCapabilities[] rendererCapabilities) {
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
|
||||||
this.downloadType = downloadType;
|
this.downloadType = downloadType;
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.cacheKey = cacheKey;
|
this.cacheKey = cacheKey;
|
||||||
|
this.mediaSource = mediaSource;
|
||||||
this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory());
|
this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory());
|
||||||
this.rendererCapabilities = Util.getRendererCapabilities(renderersFactory, drmSessionManager);
|
this.rendererCapabilities = rendererCapabilities;
|
||||||
this.scratchSet = new SparseIntArray();
|
this.scratchSet = new SparseIntArray();
|
||||||
trackSelector.setParameters(trackSelectorParameters);
|
trackSelector.setParameters(trackSelectorParameters);
|
||||||
trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter());
|
trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter());
|
||||||
@ -148,43 +361,49 @@ public abstract class DownloadHelper<T> {
|
|||||||
* @param callback A callback to be notified when preparation completes or fails. The callback
|
* @param callback A callback to be notified when preparation completes or fails. The callback
|
||||||
* will be invoked on the calling thread unless that thread does not have an associated {@link
|
* will be invoked on the calling thread unless that thread does not have an associated {@link
|
||||||
* Looper}, in which case it will be called on the application's main thread.
|
* Looper}, in which case it will be called on the application's main thread.
|
||||||
|
* @throws IllegalStateException If the download helper has already been prepared.
|
||||||
*/
|
*/
|
||||||
public final void prepare(Callback callback) {
|
public void prepare(Callback callback) {
|
||||||
Handler handler =
|
Assertions.checkState(this.callback == null);
|
||||||
|
this.callback = callback;
|
||||||
|
callbackHandler =
|
||||||
new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper());
|
new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper());
|
||||||
new Thread(
|
if (mediaSource != null) {
|
||||||
() -> {
|
mediaPreparer = new MediaPreparer(mediaSource, /* downloadHelper= */ this);
|
||||||
try {
|
} else {
|
||||||
manifest = loadManifest(uri);
|
callbackHandler.post(() -> callback.onPrepared(this));
|
||||||
trackGroupArrays = getTrackGroupArrays(manifest);
|
|
||||||
initializeTrackSelectionLists(trackGroupArrays.length, rendererCapabilities.length);
|
|
||||||
mappedTrackInfos = new MappedTrackInfo[trackGroupArrays.length];
|
|
||||||
for (int i = 0; i < trackGroupArrays.length; i++) {
|
|
||||||
TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i);
|
|
||||||
trackSelector.onSelectionActivated(trackSelectorResult.info);
|
|
||||||
mappedTrackInfos[i] =
|
|
||||||
Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
|
|
||||||
}
|
}
|
||||||
handler.post(() -> callback.onPrepared(DownloadHelper.this));
|
|
||||||
} catch (final IOException e) {
|
|
||||||
handler.post(() -> callback.onPrepareError(DownloadHelper.this, e));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the manifest. Must not be called until after preparation completes. */
|
/** Releases the helper and all resources it is holding. */
|
||||||
public final T getManifest() {
|
public void release() {
|
||||||
Assertions.checkNotNull(manifest);
|
if (mediaPreparer != null) {
|
||||||
return manifest;
|
mediaPreparer.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the manifest, or null if no manifest is loaded. Must not be called until after
|
||||||
|
* preparation completes.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public Object getManifest() {
|
||||||
|
if (mediaSource == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
assertPreparedWithMedia();
|
||||||
|
return mediaPreparer.manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of periods for which media is available. Must not be called until after
|
* Returns the number of periods for which media is available. Must not be called until after
|
||||||
* preparation completes.
|
* preparation completes.
|
||||||
*/
|
*/
|
||||||
public final int getPeriodCount() {
|
public int getPeriodCount() {
|
||||||
Assertions.checkNotNull(trackGroupArrays);
|
if (mediaSource == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
assertPreparedWithMedia();
|
||||||
return trackGroupArrays.length;
|
return trackGroupArrays.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,8 +417,8 @@ public abstract class DownloadHelper<T> {
|
|||||||
* @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream
|
* @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream
|
||||||
* content.
|
* content.
|
||||||
*/
|
*/
|
||||||
public final TrackGroupArray getTrackGroups(int periodIndex) {
|
public TrackGroupArray getTrackGroups(int periodIndex) {
|
||||||
Assertions.checkNotNull(trackGroupArrays);
|
assertPreparedWithMedia();
|
||||||
return trackGroupArrays[periodIndex];
|
return trackGroupArrays[periodIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,8 +429,8 @@ public abstract class DownloadHelper<T> {
|
|||||||
* @param periodIndex The period index.
|
* @param periodIndex The period index.
|
||||||
* @return The {@link MappedTrackInfo} for the period.
|
* @return The {@link MappedTrackInfo} for the period.
|
||||||
*/
|
*/
|
||||||
public final MappedTrackInfo getMappedTrackInfo(int periodIndex) {
|
public MappedTrackInfo getMappedTrackInfo(int periodIndex) {
|
||||||
Assertions.checkNotNull(mappedTrackInfos);
|
assertPreparedWithMedia();
|
||||||
return mappedTrackInfos[periodIndex];
|
return mappedTrackInfos[periodIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,8 +442,8 @@ public abstract class DownloadHelper<T> {
|
|||||||
* @param rendererIndex The renderer index.
|
* @param rendererIndex The renderer index.
|
||||||
* @return A list of selected {@link TrackSelection track selections}.
|
* @return A list of selected {@link TrackSelection track selections}.
|
||||||
*/
|
*/
|
||||||
public final List<TrackSelection> getTrackSelections(int periodIndex, int rendererIndex) {
|
public List<TrackSelection> getTrackSelections(int periodIndex, int rendererIndex) {
|
||||||
Assertions.checkNotNull(immutableTrackSelectionsByPeriodAndRenderer);
|
assertPreparedWithMedia();
|
||||||
return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
|
return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,8 +453,8 @@ public abstract class DownloadHelper<T> {
|
|||||||
*
|
*
|
||||||
* @param periodIndex The period index for which track selections are cleared.
|
* @param periodIndex The period index for which track selections are cleared.
|
||||||
*/
|
*/
|
||||||
public final void clearTrackSelections(int periodIndex) {
|
public void clearTrackSelections(int periodIndex) {
|
||||||
Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
|
assertPreparedWithMedia();
|
||||||
for (int i = 0; i < rendererCapabilities.length; i++) {
|
for (int i = 0; i < rendererCapabilities.length; i++) {
|
||||||
trackSelectionsByPeriodAndRenderer[periodIndex][i].clear();
|
trackSelectionsByPeriodAndRenderer[periodIndex][i].clear();
|
||||||
}
|
}
|
||||||
@ -249,7 +468,7 @@ public abstract class DownloadHelper<T> {
|
|||||||
* @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
|
* @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
|
||||||
* selection of tracks.
|
* selection of tracks.
|
||||||
*/
|
*/
|
||||||
public final void replaceTrackSelections(
|
public void replaceTrackSelections(
|
||||||
int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
|
int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
|
||||||
clearTrackSelections(periodIndex);
|
clearTrackSelections(periodIndex);
|
||||||
addTrackSelection(periodIndex, trackSelectorParameters);
|
addTrackSelection(periodIndex, trackSelectorParameters);
|
||||||
@ -263,14 +482,71 @@ public abstract class DownloadHelper<T> {
|
|||||||
* @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
|
* @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
|
||||||
* selection of tracks.
|
* selection of tracks.
|
||||||
*/
|
*/
|
||||||
public final void addTrackSelection(
|
public void addTrackSelection(
|
||||||
int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
|
int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
|
||||||
Assertions.checkNotNull(trackGroupArrays);
|
assertPreparedWithMedia();
|
||||||
Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
|
|
||||||
trackSelector.setParameters(trackSelectorParameters);
|
trackSelector.setParameters(trackSelectorParameters);
|
||||||
runTrackSelection(periodIndex);
|
runTrackSelection(periodIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to add selections of tracks for all specified audio languages. If an audio
|
||||||
|
* track in one of the specified languages is not available, the default fallback audio track is
|
||||||
|
* used instead. Must not be called until after preparation completes.
|
||||||
|
*
|
||||||
|
* @param languages A list of audio languages for which tracks should be added to the download
|
||||||
|
* selection, as ISO 639-1 two-letter or ISO 639-2 three-letter codes.
|
||||||
|
*/
|
||||||
|
public void addAudioLanguagesToSelection(String... languages) {
|
||||||
|
assertPreparedWithMedia();
|
||||||
|
for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) {
|
||||||
|
DefaultTrackSelector.ParametersBuilder parametersBuilder =
|
||||||
|
DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon();
|
||||||
|
MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex];
|
||||||
|
int rendererCount = mappedTrackInfo.getRendererCount();
|
||||||
|
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
|
||||||
|
if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_AUDIO) {
|
||||||
|
parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String language : languages) {
|
||||||
|
parametersBuilder.setPreferredAudioLanguage(language);
|
||||||
|
addTrackSelection(periodIndex, parametersBuilder.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method to add selections of tracks for all specified text languages. Must not be
|
||||||
|
* called until after preparation completes.
|
||||||
|
*
|
||||||
|
* @param selectUndeterminedTextLanguage Whether a text track with undetermined language should be
|
||||||
|
* selected for downloading if no track with one of the specified {@code languages} is
|
||||||
|
* available.
|
||||||
|
* @param languages A list of text languages for which tracks should be added to the download
|
||||||
|
* selection, as ISO 639-1 two-letter or ISO 639-2 three-letter codes.
|
||||||
|
*/
|
||||||
|
public void addTextLanguagesToSelection(
|
||||||
|
boolean selectUndeterminedTextLanguage, String... languages) {
|
||||||
|
assertPreparedWithMedia();
|
||||||
|
for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) {
|
||||||
|
DefaultTrackSelector.ParametersBuilder parametersBuilder =
|
||||||
|
DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon();
|
||||||
|
MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex];
|
||||||
|
int rendererCount = mappedTrackInfo.getRendererCount();
|
||||||
|
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
|
||||||
|
if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_TEXT) {
|
||||||
|
parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parametersBuilder.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage);
|
||||||
|
for (String language : languages) {
|
||||||
|
parametersBuilder.setPreferredTextLanguage(language);
|
||||||
|
addTrackSelection(periodIndex, parametersBuilder.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a {@link DownloadAction} for downloading the selected tracks. Must not be called until
|
* Builds a {@link DownloadAction} for downloading the selected tracks. Must not be called until
|
||||||
* after preparation completes.
|
* after preparation completes.
|
||||||
@ -278,27 +554,22 @@ public abstract class DownloadHelper<T> {
|
|||||||
* @param data Application provided data to store in {@link DownloadAction#data}.
|
* @param data Application provided data to store in {@link DownloadAction#data}.
|
||||||
* @return The built {@link DownloadAction}.
|
* @return The built {@link DownloadAction}.
|
||||||
*/
|
*/
|
||||||
public final DownloadAction getDownloadAction(@Nullable byte[] data) {
|
public DownloadAction getDownloadAction(@Nullable byte[] data) {
|
||||||
Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
|
if (mediaSource == null) {
|
||||||
Assertions.checkNotNull(trackGroupArrays);
|
return DownloadAction.createDownloadAction(
|
||||||
|
downloadType, uri, /* keys= */ Collections.emptyList(), cacheKey, data);
|
||||||
|
}
|
||||||
|
assertPreparedWithMedia();
|
||||||
List<StreamKey> streamKeys = new ArrayList<>();
|
List<StreamKey> streamKeys = new ArrayList<>();
|
||||||
|
List<TrackSelection> allSelections = new ArrayList<>();
|
||||||
int periodCount = trackSelectionsByPeriodAndRenderer.length;
|
int periodCount = trackSelectionsByPeriodAndRenderer.length;
|
||||||
for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
|
for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
|
||||||
|
allSelections.clear();
|
||||||
int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length;
|
int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length;
|
||||||
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
|
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
|
||||||
List<TrackSelection> trackSelectionList =
|
allSelections.addAll(trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]);
|
||||||
trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
|
|
||||||
for (int selectionIndex = 0; selectionIndex < trackSelectionList.size(); selectionIndex++) {
|
|
||||||
TrackSelection trackSelection = trackSelectionList.get(selectionIndex);
|
|
||||||
int trackGroupIndex =
|
|
||||||
trackGroupArrays[periodIndex].indexOf(trackSelection.getTrackGroup());
|
|
||||||
int trackCount = trackSelection.length();
|
|
||||||
for (int trackListIndex = 0; trackListIndex < trackCount; trackListIndex++) {
|
|
||||||
int trackIndex = trackSelection.getIndexInTrackGroup(trackListIndex);
|
|
||||||
streamKeys.add(toStreamKey(periodIndex, trackGroupIndex, trackIndex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections));
|
||||||
}
|
}
|
||||||
return DownloadAction.createDownloadAction(downloadType, uri, streamKeys, cacheKey, data);
|
return DownloadAction.createDownloadAction(downloadType, uri, streamKeys, cacheKey, data);
|
||||||
}
|
}
|
||||||
@ -308,40 +579,18 @@ public abstract class DownloadHelper<T> {
|
|||||||
*
|
*
|
||||||
* @return The built {@link DownloadAction}.
|
* @return The built {@link DownloadAction}.
|
||||||
*/
|
*/
|
||||||
public final DownloadAction getRemoveAction() {
|
public DownloadAction getRemoveAction() {
|
||||||
return DownloadAction.createRemoveAction(downloadType, uri, cacheKey);
|
return DownloadAction.createRemoveAction(downloadType, uri, cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Initialization of array of Lists.
|
||||||
* Loads the manifest. This method is called on a background thread.
|
|
||||||
*
|
|
||||||
* @param uri The manifest uri.
|
|
||||||
* @throws IOException If loading fails.
|
|
||||||
*/
|
|
||||||
protected abstract T loadManifest(Uri uri) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the track group arrays for each period in the manifest.
|
|
||||||
*
|
|
||||||
* @param manifest The manifest.
|
|
||||||
* @return An array of {@link TrackGroupArray}s. One for each period in the manifest.
|
|
||||||
*/
|
|
||||||
protected abstract TrackGroupArray[] getTrackGroupArrays(T manifest);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a track of a track group of a period to the corresponding {@link StreamKey}.
|
|
||||||
*
|
|
||||||
* @param periodIndex The index of the containing period.
|
|
||||||
* @param trackGroupIndex The index of the containing track group within the period.
|
|
||||||
* @param trackIndexInTrackGroup The index of the track within the track group.
|
|
||||||
* @return The corresponding {@link StreamKey}.
|
|
||||||
*/
|
|
||||||
protected abstract StreamKey toStreamKey(
|
|
||||||
int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup);
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@EnsuresNonNull("trackSelectionsByPeriodAndRenderer")
|
private void onMediaPrepared() {
|
||||||
private void initializeTrackSelectionLists(int periodCount, int rendererCount) {
|
Assertions.checkNotNull(mediaPreparer);
|
||||||
|
Assertions.checkNotNull(mediaPreparer.mediaPeriods);
|
||||||
|
Assertions.checkNotNull(mediaPreparer.timeline);
|
||||||
|
int periodCount = mediaPreparer.mediaPeriods.length;
|
||||||
|
int rendererCount = rendererCapabilities.length;
|
||||||
trackSelectionsByPeriodAndRenderer =
|
trackSelectionsByPeriodAndRenderer =
|
||||||
(List<TrackSelection>[][]) new List<?>[periodCount][rendererCount];
|
(List<TrackSelection>[][]) new List<?>[periodCount][rendererCount];
|
||||||
immutableTrackSelectionsByPeriodAndRenderer =
|
immutableTrackSelectionsByPeriodAndRenderer =
|
||||||
@ -353,6 +602,49 @@ public abstract class DownloadHelper<T> {
|
|||||||
Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]);
|
Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
trackGroupArrays = new TrackGroupArray[periodCount];
|
||||||
|
mappedTrackInfos = new MappedTrackInfo[periodCount];
|
||||||
|
for (int i = 0; i < periodCount; i++) {
|
||||||
|
trackGroupArrays[i] = mediaPreparer.mediaPeriods[i].getTrackGroups();
|
||||||
|
TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i);
|
||||||
|
trackSelector.onSelectionActivated(trackSelectorResult.info);
|
||||||
|
mappedTrackInfos[i] = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
|
||||||
|
}
|
||||||
|
setPreparedWithMedia();
|
||||||
|
Assertions.checkNotNull(callbackHandler)
|
||||||
|
.post(() -> Assertions.checkNotNull(callback).onPrepared(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onMediaPreparationFailed(IOException error) {
|
||||||
|
Assertions.checkNotNull(callbackHandler)
|
||||||
|
.post(() -> Assertions.checkNotNull(callback).onPrepareError(this, error));
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresNonNull({
|
||||||
|
"trackGroupArrays",
|
||||||
|
"mappedTrackInfos",
|
||||||
|
"trackSelectionsByPeriodAndRenderer",
|
||||||
|
"immutableTrackSelectionsByPeriodAndRenderer",
|
||||||
|
"mediaPreparer",
|
||||||
|
"mediaPreparer.timeline",
|
||||||
|
"mediaPreparer.mediaPeriods"
|
||||||
|
})
|
||||||
|
private void setPreparedWithMedia() {
|
||||||
|
isPreparedWithMedia = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnsuresNonNull({
|
||||||
|
"trackGroupArrays",
|
||||||
|
"mappedTrackInfos",
|
||||||
|
"trackSelectionsByPeriodAndRenderer",
|
||||||
|
"immutableTrackSelectionsByPeriodAndRenderer",
|
||||||
|
"mediaPreparer",
|
||||||
|
"mediaPreparer.timeline",
|
||||||
|
"mediaPreparer.mediaPeriods"
|
||||||
|
})
|
||||||
|
@SuppressWarnings("nullness:contracts.postcondition.not.satisfied")
|
||||||
|
private void assertPreparedWithMedia() {
|
||||||
|
Assertions.checkState(isPreparedWithMedia);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -361,26 +653,27 @@ public abstract class DownloadHelper<T> {
|
|||||||
*/
|
*/
|
||||||
// Intentional reference comparison of track group instances.
|
// Intentional reference comparison of track group instances.
|
||||||
@SuppressWarnings("ReferenceEquality")
|
@SuppressWarnings("ReferenceEquality")
|
||||||
@RequiresNonNull({"trackGroupArrays", "trackSelectionsByPeriodAndRenderer"})
|
@RequiresNonNull({
|
||||||
|
"trackGroupArrays",
|
||||||
|
"trackSelectionsByPeriodAndRenderer",
|
||||||
|
"mediaPreparer",
|
||||||
|
"mediaPreparer.timeline"
|
||||||
|
})
|
||||||
private TrackSelectorResult runTrackSelection(int periodIndex) {
|
private TrackSelectorResult runTrackSelection(int periodIndex) {
|
||||||
// TODO: Use actual timeline and media period id.
|
|
||||||
MediaPeriodId dummyMediaPeriodId = new MediaPeriodId(new Object());
|
|
||||||
Timeline dummyTimeline = Timeline.EMPTY;
|
|
||||||
currentTrackSelectionPeriodIndex = periodIndex;
|
|
||||||
try {
|
try {
|
||||||
TrackSelectorResult trackSelectorResult =
|
TrackSelectorResult trackSelectorResult =
|
||||||
trackSelector.selectTracks(
|
trackSelector.selectTracks(
|
||||||
rendererCapabilities,
|
rendererCapabilities,
|
||||||
trackGroupArrays[periodIndex],
|
trackGroupArrays[periodIndex],
|
||||||
dummyMediaPeriodId,
|
new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)),
|
||||||
dummyTimeline);
|
mediaPreparer.timeline);
|
||||||
for (int i = 0; i < trackSelectorResult.length; i++) {
|
for (int i = 0; i < trackSelectorResult.length; i++) {
|
||||||
TrackSelection newSelection = trackSelectorResult.selections.get(i);
|
TrackSelection newSelection = trackSelectorResult.selections.get(i);
|
||||||
if (newSelection == null) {
|
if (newSelection == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
List<TrackSelection> existingSelectionList =
|
List<TrackSelection> existingSelectionList =
|
||||||
trackSelectionsByPeriodAndRenderer[currentTrackSelectionPeriodIndex][i];
|
trackSelectionsByPeriodAndRenderer[periodIndex][i];
|
||||||
boolean mergedWithExistingSelection = false;
|
boolean mergedWithExistingSelection = false;
|
||||||
for (int j = 0; j < existingSelectionList.size(); j++) {
|
for (int j = 0; j < existingSelectionList.size(); j++) {
|
||||||
TrackSelection existingSelection = existingSelectionList.get(j);
|
TrackSelection existingSelection = existingSelectionList.get(j);
|
||||||
@ -414,6 +707,156 @@ public abstract class DownloadHelper<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Pair<@NullableType Constructor<?>, @NullableType Method>
|
||||||
|
getMediaSourceFactoryMethods(String className) {
|
||||||
|
Constructor<?> constructor = null;
|
||||||
|
Method createMethod = null;
|
||||||
|
try {
|
||||||
|
// LINT.IfChange
|
||||||
|
Class<?> factoryClazz = Class.forName(className);
|
||||||
|
constructor = factoryClazz.getConstructor(DataSource.Factory.class);
|
||||||
|
createMethod = factoryClazz.getMethod("createMediaSource", Uri.class);
|
||||||
|
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Expected if the app was built without the respective module.
|
||||||
|
}
|
||||||
|
return Pair.create(constructor, createMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaSource createMediaSource(
|
||||||
|
Uri uri,
|
||||||
|
DataSource.Factory dataSourceFactory,
|
||||||
|
@Nullable Constructor<?> factoryConstructor,
|
||||||
|
@Nullable Method createMediaSourceMethod) {
|
||||||
|
if (factoryConstructor == null || createMediaSourceMethod == null) {
|
||||||
|
throw new IllegalStateException("Module missing to create media source.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Object factory = factoryConstructor.newInstance(dataSourceFactory);
|
||||||
|
return (MediaSource) Assertions.checkNotNull(createMediaSourceMethod.invoke(factory, uri));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Failed to instantiate media source.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MediaPreparer
|
||||||
|
implements MediaSource.SourceInfoRefreshListener, MediaPeriod.Callback, Handler.Callback {
|
||||||
|
|
||||||
|
private static final int MESSAGE_PREPARE_SOURCE = 0;
|
||||||
|
private static final int MESSAGE_CHECK_FOR_FAILURE = 1;
|
||||||
|
private static final int MESSAGE_CONTINUE_LOADING = 2;
|
||||||
|
|
||||||
|
private final MediaSource mediaSource;
|
||||||
|
private final DownloadHelper downloadHelper;
|
||||||
|
private final Allocator allocator;
|
||||||
|
private final HandlerThread mediaSourceThread;
|
||||||
|
private final Handler mediaSourceHandler;
|
||||||
|
|
||||||
|
@Nullable public Object manifest;
|
||||||
|
public @MonotonicNonNull Timeline timeline;
|
||||||
|
public MediaPeriod @MonotonicNonNull [] mediaPeriods;
|
||||||
|
|
||||||
|
private final ArrayList<MediaPeriod> pendingMediaPeriods;
|
||||||
|
|
||||||
|
public MediaPreparer(MediaSource mediaSource, DownloadHelper downloadHelper) {
|
||||||
|
this.mediaSource = mediaSource;
|
||||||
|
this.downloadHelper = downloadHelper;
|
||||||
|
allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||||
|
mediaSourceThread = new HandlerThread("DownloadHelper");
|
||||||
|
mediaSourceThread.start();
|
||||||
|
mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this);
|
||||||
|
mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE);
|
||||||
|
pendingMediaPeriods = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release() {
|
||||||
|
if (mediaPeriods != null) {
|
||||||
|
for (MediaPeriod mediaPeriod : mediaPeriods) {
|
||||||
|
mediaSource.releasePeriod(mediaPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaSource.releaseSource(this);
|
||||||
|
mediaSourceThread.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler.Callback
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleMessage(Message msg) {
|
||||||
|
switch (msg.what) {
|
||||||
|
case MESSAGE_PREPARE_SOURCE:
|
||||||
|
mediaSource.prepareSource(/* listener= */ this, /* mediaTransferListener= */ null);
|
||||||
|
mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE);
|
||||||
|
return true;
|
||||||
|
case MESSAGE_CHECK_FOR_FAILURE:
|
||||||
|
try {
|
||||||
|
if (mediaPeriods == null) {
|
||||||
|
mediaSource.maybeThrowSourceInfoRefreshError();
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < pendingMediaPeriods.size(); i++) {
|
||||||
|
pendingMediaPeriods.get(i).maybeThrowPrepareError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaSourceHandler.sendEmptyMessageDelayed(
|
||||||
|
MESSAGE_CHECK_FOR_FAILURE, /* delayMillis= */ 100);
|
||||||
|
} catch (IOException e) {
|
||||||
|
downloadHelper.onMediaPreparationFailed(e);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case MESSAGE_CONTINUE_LOADING:
|
||||||
|
MediaPeriod mediaPeriod = (MediaPeriod) msg.obj;
|
||||||
|
if (pendingMediaPeriods.contains(mediaPeriod)) {
|
||||||
|
mediaPeriod.continueLoading(/* positionUs= */ 0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaSource.SourceInfoRefreshListener implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSourceInfoRefreshed(
|
||||||
|
MediaSource source, Timeline timeline, @Nullable Object manifest) {
|
||||||
|
if (this.timeline != null) {
|
||||||
|
// Ignore dynamic updates.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.timeline = timeline;
|
||||||
|
this.manifest = manifest;
|
||||||
|
mediaPeriods = new MediaPeriod[timeline.getPeriodCount()];
|
||||||
|
for (int i = 0; i < mediaPeriods.length; i++) {
|
||||||
|
MediaPeriod mediaPeriod =
|
||||||
|
mediaSource.createPeriod(
|
||||||
|
new MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ i)),
|
||||||
|
allocator,
|
||||||
|
/* startPositionUs= */ 0);
|
||||||
|
mediaPeriods[i] = mediaPeriod;
|
||||||
|
pendingMediaPeriods.add(mediaPeriod);
|
||||||
|
mediaPeriod.prepare(/* callback= */ this, /* positionUs= */ 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaPeriod.Callback implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
pendingMediaPeriods.remove(mediaPeriod);
|
||||||
|
if (pendingMediaPeriods.isEmpty()) {
|
||||||
|
mediaSourceHandler.removeMessages(MESSAGE_CHECK_FOR_FAILURE);
|
||||||
|
downloadHelper.onMediaPrepared();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod mediaPeriod) {
|
||||||
|
if (pendingMediaPeriods.contains(mediaPeriod)) {
|
||||||
|
mediaSourceHandler.obtainMessage(MESSAGE_CONTINUE_LOADING, mediaPeriod).sendToTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class DownloadTrackSelection extends BaseTrackSelection {
|
private static final class DownloadTrackSelection extends BaseTrackSelection {
|
||||||
|
|
||||||
private static final class Factory implements TrackSelection.Factory {
|
private static final class Factory implements TrackSelection.Factory {
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.offline;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
/** Persists {@link DownloadState}s. */
|
||||||
|
interface DownloadIndex {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link DownloadState} with the given {@code id}, or null.
|
||||||
|
*
|
||||||
|
* @param id ID of a {@link DownloadState}.
|
||||||
|
* @return The {@link DownloadState} with the given {@code id}, or null if a download state with
|
||||||
|
* this id doesn't exist.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
DownloadState getDownloadState(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link DownloadStateCursor} to {@link DownloadState}s with the given {@code states}.
|
||||||
|
*
|
||||||
|
* @param states Returns only the {@link DownloadState}s with this states. If empty, returns all.
|
||||||
|
* @return A cursor to {@link DownloadState}s with the given {@code states}.
|
||||||
|
*/
|
||||||
|
DownloadStateCursor getDownloadStates(@DownloadState.State int... states);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or replaces a {@link DownloadState}.
|
||||||
|
*
|
||||||
|
* @param downloadState The {@link DownloadState} to be added.
|
||||||
|
*/
|
||||||
|
void putDownloadState(DownloadState downloadState);
|
||||||
|
|
||||||
|
/** Removes the {@link DownloadState} with the given {@code id}. */
|
||||||
|
void removeDownloadState(String id);
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.offline;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/** {@link DownloadIndex} related utility methods. */
|
||||||
|
public final class DownloadIndexUtil {
|
||||||
|
|
||||||
|
/** An interface to provide custom download ids during ActionFile upgrade. */
|
||||||
|
public interface DownloadIdProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a custom download id for given action.
|
||||||
|
*
|
||||||
|
* @param downloadAction The action which is an id requested for.
|
||||||
|
* @return A custom download id for given action.
|
||||||
|
*/
|
||||||
|
String getId(DownloadAction downloadAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DownloadIndexUtil() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrades an {@link ActionFile} to {@link DownloadIndex}.
|
||||||
|
*
|
||||||
|
* <p>This method shouldn't be called while {@link DownloadIndex} is used by {@link
|
||||||
|
* DownloadManager}.
|
||||||
|
*
|
||||||
|
* @param actionFile The action file to upgrade.
|
||||||
|
* @param downloadIndex Actions are converted to {@link DownloadState}s and stored in this index.
|
||||||
|
* @param downloadIdProvider A nullable custom download id provider.
|
||||||
|
* @throws IOException If there is an error during loading actions.
|
||||||
|
*/
|
||||||
|
public static void upgradeActionFile(
|
||||||
|
ActionFile actionFile,
|
||||||
|
DownloadIndex downloadIndex,
|
||||||
|
@Nullable DownloadIdProvider downloadIdProvider)
|
||||||
|
throws IOException {
|
||||||
|
if (downloadIdProvider == null) {
|
||||||
|
downloadIdProvider = downloadAction -> downloadAction.id;
|
||||||
|
}
|
||||||
|
for (DownloadAction action : actionFile.load()) {
|
||||||
|
addAction(downloadIndex, downloadIdProvider.getId(action), action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a {@link DownloadAction} to {@link DownloadState} and stored in the given {@link
|
||||||
|
* DownloadIndex}.
|
||||||
|
*
|
||||||
|
* <p>This method shouldn't be called while {@link DownloadIndex} is used by {@link
|
||||||
|
* DownloadManager}.
|
||||||
|
*
|
||||||
|
* @param downloadIndex The action is converted to {@link DownloadState} and stored in this index.
|
||||||
|
* @param id A nullable custom download id which overwrites {@link DownloadAction#id}.
|
||||||
|
* @param action The action to be stored in {@link DownloadIndex}.
|
||||||
|
*/
|
||||||
|
public static void addAction(
|
||||||
|
DownloadIndex downloadIndex, @Nullable String id, DownloadAction action) {
|
||||||
|
DownloadState downloadState = downloadIndex.getDownloadState(id != null ? id : action.id);
|
||||||
|
if (downloadState != null) {
|
||||||
|
downloadState = downloadState.mergeAction(action);
|
||||||
|
} else {
|
||||||
|
downloadState = new DownloadState(action);
|
||||||
|
}
|
||||||
|
downloadIndex.putDownloadState(downloadState);
|
||||||
|
}
|
||||||
|
}
|
@ -25,21 +25,28 @@ import static com.google.android.exoplayer2.offline.DownloadState.STATE_REMOVED;
|
|||||||
import static com.google.android.exoplayer2.offline.DownloadState.STATE_REMOVING;
|
import static com.google.android.exoplayer2.offline.DownloadState.STATE_REMOVING;
|
||||||
import static com.google.android.exoplayer2.offline.DownloadState.STATE_RESTARTING;
|
import static com.google.android.exoplayer2.offline.DownloadState.STATE_RESTARTING;
|
||||||
import static com.google.android.exoplayer2.offline.DownloadState.STATE_STOPPED;
|
import static com.google.android.exoplayer2.offline.DownloadState.STATE_STOPPED;
|
||||||
import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY;
|
import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_MANUAL;
|
||||||
import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_STOPPED;
|
import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_REQUIREMENTS_NOT_MET;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.os.ConditionVariable;
|
import android.os.ConditionVariable;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.scheduler.Requirements;
|
||||||
|
import com.google.android.exoplayer2.scheduler.RequirementsWatcher;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
@ -74,22 +81,55 @@ public final class DownloadManager {
|
|||||||
* @param downloadManager The reporting instance.
|
* @param downloadManager The reporting instance.
|
||||||
*/
|
*/
|
||||||
void onIdle(DownloadManager downloadManager);
|
void onIdle(DownloadManager downloadManager);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the download requirements state changed.
|
||||||
|
*
|
||||||
|
* @param downloadManager The reporting instance.
|
||||||
|
* @param requirements Requirements needed to be met to start downloads.
|
||||||
|
* @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not
|
||||||
|
* met, or 0.
|
||||||
|
*/
|
||||||
|
void onRequirementsStateChanged(
|
||||||
|
DownloadManager downloadManager,
|
||||||
|
Requirements requirements,
|
||||||
|
@Requirements.RequirementFlags int notMetRequirements);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The default maximum number of simultaneous downloads. */
|
/** The default maximum number of simultaneous downloads. */
|
||||||
public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1;
|
public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1;
|
||||||
/** The default minimum number of times a download must be retried before failing. */
|
/** The default minimum number of times a download must be retried before failing. */
|
||||||
public static final int DEFAULT_MIN_RETRY_COUNT = 5;
|
public static final int DEFAULT_MIN_RETRY_COUNT = 5;
|
||||||
|
/** The default requirement is that the device has network connectivity. */
|
||||||
|
public static final Requirements DEFAULT_REQUIREMENTS =
|
||||||
|
new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@IntDef({
|
||||||
|
START_THREAD_SUCCEEDED,
|
||||||
|
START_THREAD_WAIT_REMOVAL_TO_FINISH,
|
||||||
|
START_THREAD_WAIT_DOWNLOAD_CANCELLATION,
|
||||||
|
START_THREAD_TOO_MANY_DOWNLOADS,
|
||||||
|
START_THREAD_NOT_ALLOWED
|
||||||
|
})
|
||||||
|
private @interface StartThreadResults {}
|
||||||
|
|
||||||
|
private static final int START_THREAD_SUCCEEDED = 0;
|
||||||
|
private static final int START_THREAD_WAIT_REMOVAL_TO_FINISH = 1;
|
||||||
|
private static final int START_THREAD_WAIT_DOWNLOAD_CANCELLATION = 2;
|
||||||
|
private static final int START_THREAD_TOO_MANY_DOWNLOADS = 3;
|
||||||
|
private static final int START_THREAD_NOT_ALLOWED = 4;
|
||||||
|
|
||||||
private static final String TAG = "DownloadManager";
|
private static final String TAG = "DownloadManager";
|
||||||
private static final boolean DEBUG = false;
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
private final int maxActiveDownloads;
|
private final int maxSimultaneousDownloads;
|
||||||
private final int minRetryCount;
|
private final int minRetryCount;
|
||||||
|
private final Context context;
|
||||||
private final ActionFile actionFile;
|
private final ActionFile actionFile;
|
||||||
private final DownloaderFactory downloaderFactory;
|
private final DownloaderFactory downloaderFactory;
|
||||||
private final ArrayList<Download> downloads;
|
private final ArrayList<Download> downloads;
|
||||||
private final ArrayList<Download> activeDownloads;
|
private final HashMap<Download, DownloadThread> activeDownloads;
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
private final HandlerThread fileIOThread;
|
private final HandlerThread fileIOThread;
|
||||||
private final Handler fileIOHandler;
|
private final Handler fileIOHandler;
|
||||||
@ -98,40 +138,55 @@ public final class DownloadManager {
|
|||||||
|
|
||||||
private boolean initialized;
|
private boolean initialized;
|
||||||
private boolean released;
|
private boolean released;
|
||||||
@DownloadState.StopFlags private int stickyStopFlags;
|
@DownloadState.StopFlags private int stopFlags;
|
||||||
|
@Requirements.RequirementFlags private int notMetRequirements;
|
||||||
|
private int manualStopReason;
|
||||||
|
private RequirementsWatcher requirementsWatcher;
|
||||||
|
private int simultaneousDownloads;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a {@link DownloadManager}.
|
* Constructs a {@link DownloadManager}.
|
||||||
*
|
*
|
||||||
|
* @param context Any context.
|
||||||
* @param actionFile The file in which active actions are saved.
|
* @param actionFile The file in which active actions are saved.
|
||||||
* @param downloaderFactory A factory for creating {@link Downloader}s.
|
* @param downloaderFactory A factory for creating {@link Downloader}s.
|
||||||
*/
|
*/
|
||||||
public DownloadManager(File actionFile, DownloaderFactory downloaderFactory) {
|
public DownloadManager(Context context, File actionFile, DownloaderFactory downloaderFactory) {
|
||||||
this(
|
this(
|
||||||
actionFile, downloaderFactory, DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, DEFAULT_MIN_RETRY_COUNT);
|
context,
|
||||||
|
actionFile,
|
||||||
|
downloaderFactory,
|
||||||
|
DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS,
|
||||||
|
DEFAULT_MIN_RETRY_COUNT,
|
||||||
|
DEFAULT_REQUIREMENTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a {@link DownloadManager}.
|
* Constructs a {@link DownloadManager}.
|
||||||
*
|
*
|
||||||
|
* @param context Any context.
|
||||||
* @param actionFile The file in which active actions are saved.
|
* @param actionFile The file in which active actions are saved.
|
||||||
* @param downloaderFactory A factory for creating {@link Downloader}s.
|
* @param downloaderFactory A factory for creating {@link Downloader}s.
|
||||||
* @param maxSimultaneousDownloads The maximum number of simultaneous downloads.
|
* @param maxSimultaneousDownloads The maximum number of simultaneous downloads.
|
||||||
* @param minRetryCount The minimum number of times a download must be retried before failing.
|
* @param minRetryCount The minimum number of times a download must be retried before failing.
|
||||||
|
* @param requirements The requirements needed to be met to start downloads.
|
||||||
*/
|
*/
|
||||||
public DownloadManager(
|
public DownloadManager(
|
||||||
|
Context context,
|
||||||
File actionFile,
|
File actionFile,
|
||||||
DownloaderFactory downloaderFactory,
|
DownloaderFactory downloaderFactory,
|
||||||
int maxSimultaneousDownloads,
|
int maxSimultaneousDownloads,
|
||||||
int minRetryCount) {
|
int minRetryCount,
|
||||||
|
Requirements requirements) {
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
this.actionFile = new ActionFile(actionFile);
|
this.actionFile = new ActionFile(actionFile);
|
||||||
this.downloaderFactory = downloaderFactory;
|
this.downloaderFactory = downloaderFactory;
|
||||||
this.maxActiveDownloads = maxSimultaneousDownloads;
|
this.maxSimultaneousDownloads = maxSimultaneousDownloads;
|
||||||
this.minRetryCount = minRetryCount;
|
this.minRetryCount = minRetryCount;
|
||||||
this.stickyStopFlags = STOP_FLAG_STOPPED | STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY;
|
|
||||||
|
|
||||||
|
stopFlags = STOP_FLAG_MANUAL;
|
||||||
downloads = new ArrayList<>();
|
downloads = new ArrayList<>();
|
||||||
activeDownloads = new ArrayList<>();
|
activeDownloads = new HashMap<>();
|
||||||
|
|
||||||
Looper looper = Looper.myLooper();
|
Looper looper = Looper.myLooper();
|
||||||
if (looper == null) {
|
if (looper == null) {
|
||||||
@ -146,10 +201,30 @@ public final class DownloadManager {
|
|||||||
listeners = new CopyOnWriteArraySet<>();
|
listeners = new CopyOnWriteArraySet<>();
|
||||||
actionQueue = new ArrayDeque<>();
|
actionQueue = new ArrayDeque<>();
|
||||||
|
|
||||||
|
setNotMetRequirements(watchRequirements(requirements));
|
||||||
loadActions();
|
loadActions();
|
||||||
logd("Created");
|
logd("Created");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the requirements needed to be met to start downloads.
|
||||||
|
*
|
||||||
|
* @param requirements Need to be met to start downloads.
|
||||||
|
*/
|
||||||
|
public void setRequirements(Requirements requirements) {
|
||||||
|
Assertions.checkState(!released);
|
||||||
|
if (requirements.equals(requirementsWatcher.getRequirements())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requirementsWatcher.stop();
|
||||||
|
onRequirementsStateChanged(watchRequirements(requirements));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the requirements needed to be met to start downloads. */
|
||||||
|
public Requirements getRequirements() {
|
||||||
|
return requirementsWatcher.getRequirements();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a {@link Listener}.
|
* Adds a {@link Listener}.
|
||||||
*
|
*
|
||||||
@ -168,33 +243,35 @@ public final class DownloadManager {
|
|||||||
listeners.remove(listener);
|
listeners.remove(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Starts the downloads. */
|
/**
|
||||||
|
* Clears {@link DownloadState#STOP_FLAG_MANUAL} flag of all downloads. Downloads are started if
|
||||||
|
* the requirements are met.
|
||||||
|
*/
|
||||||
public void startDownloads() {
|
public void startDownloads() {
|
||||||
clearStopFlags(STOP_FLAG_STOPPED);
|
logd("manual stopped is cancelled");
|
||||||
}
|
manualStopReason = 0;
|
||||||
|
stopFlags &= ~STOP_FLAG_MANUAL;
|
||||||
/** Stops all of the downloads. Call {@link #startDownloads()} to restart downloads. */
|
|
||||||
public void stopDownloads() {
|
|
||||||
setStopFlags(STOP_FLAG_STOPPED);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setStopFlags(int flags) {
|
|
||||||
updateStopFlags(flags, flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearStopFlags(int flags) {
|
|
||||||
updateStopFlags(flags, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateStopFlags(int flags, int values) {
|
|
||||||
Assertions.checkState(!released);
|
|
||||||
int updatedStickyStopFlags = (values & flags) | (stickyStopFlags & ~flags);
|
|
||||||
if (stickyStopFlags != updatedStickyStopFlags) {
|
|
||||||
stickyStopFlags = updatedStickyStopFlags;
|
|
||||||
for (int i = 0; i < downloads.size(); i++) {
|
for (int i = 0; i < downloads.size(); i++) {
|
||||||
downloads.get(i).updateStopFlags(flags, values);
|
downloads.get(i).clearManualStopReason();
|
||||||
}
|
}
|
||||||
logdFlags("Sticky stop flags are updated", updatedStickyStopFlags);
|
}
|
||||||
|
|
||||||
|
/** Signals all downloads to stop. Call {@link #startDownloads()} to let them to be started. */
|
||||||
|
public void stopDownloads() {
|
||||||
|
stopDownloads(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals all downloads to stop. Call {@link #startDownloads()} to let them to be started.
|
||||||
|
*
|
||||||
|
* @param manualStopReason An application defined stop reason.
|
||||||
|
*/
|
||||||
|
public void stopDownloads(int manualStopReason) {
|
||||||
|
logd("downloads are stopped manually");
|
||||||
|
this.manualStopReason = manualStopReason;
|
||||||
|
stopFlags |= STOP_FLAG_MANUAL;
|
||||||
|
for (int i = 0; i < downloads.size(); i++) {
|
||||||
|
downloads.get(i).setManualStopReason(this.manualStopReason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,15 +333,7 @@ public final class DownloadManager {
|
|||||||
/** Returns whether there are no active downloads. */
|
/** Returns whether there are no active downloads. */
|
||||||
public boolean isIdle() {
|
public boolean isIdle() {
|
||||||
Assertions.checkState(!released);
|
Assertions.checkState(!released);
|
||||||
if (!initialized) {
|
return initialized && activeDownloads.isEmpty();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < downloads.size(); i++) {
|
|
||||||
if (!downloads.get(i).isIdle()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -276,8 +345,11 @@ public final class DownloadManager {
|
|||||||
if (released) {
|
if (released) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setStopFlags(STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY);
|
|
||||||
released = true;
|
released = true;
|
||||||
|
stopAllDownloadThreads();
|
||||||
|
if (requirementsWatcher != null) {
|
||||||
|
requirementsWatcher.stop();
|
||||||
|
}
|
||||||
final ConditionVariable fileIOFinishedCondition = new ConditionVariable();
|
final ConditionVariable fileIOFinishedCondition = new ConditionVariable();
|
||||||
fileIOHandler.post(fileIOFinishedCondition::open);
|
fileIOHandler.post(fileIOFinishedCondition::open);
|
||||||
fileIOFinishedCondition.block();
|
fileIOFinishedCondition.block();
|
||||||
@ -293,20 +365,11 @@ public final class DownloadManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Download download =
|
Download download = new Download(this, action, stopFlags, notMetRequirements, manualStopReason);
|
||||||
new Download(this, downloaderFactory, action, minRetryCount, stickyStopFlags);
|
|
||||||
downloads.add(download);
|
downloads.add(download);
|
||||||
logd("Download is added", download);
|
logd("Download is added", download);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeStartDownload(Download download) {
|
|
||||||
if (activeDownloads.size() < maxActiveDownloads) {
|
|
||||||
if (download.start()) {
|
|
||||||
activeDownloads.add(download);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeNotifyListenersIdle() {
|
private void maybeNotifyListenersIdle() {
|
||||||
if (!isIdle()) {
|
if (!isIdle()) {
|
||||||
return;
|
return;
|
||||||
@ -321,21 +384,11 @@ public final class DownloadManager {
|
|||||||
if (released) {
|
if (released) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean idle = download.isIdle();
|
|
||||||
if (idle) {
|
|
||||||
activeDownloads.remove(download);
|
|
||||||
}
|
|
||||||
notifyListenersDownloadStateChange(download);
|
notifyListenersDownloadStateChange(download);
|
||||||
if (download.isFinished()) {
|
if (download.isFinished()) {
|
||||||
downloads.remove(download);
|
downloads.remove(download);
|
||||||
saveActions();
|
saveActions();
|
||||||
}
|
}
|
||||||
if (idle) {
|
|
||||||
for (int i = 0; i < downloads.size(); i++) {
|
|
||||||
maybeStartDownload(downloads.get(i));
|
|
||||||
}
|
|
||||||
maybeNotifyListenersIdle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyListenersDownloadStateChange(Download download) {
|
private void notifyListenersDownloadStateChange(Download download) {
|
||||||
@ -346,6 +399,27 @@ public final class DownloadManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onRequirementsStateChanged(@Requirements.RequirementFlags int notMetRequirements) {
|
||||||
|
setNotMetRequirements(notMetRequirements);
|
||||||
|
logdFlags("Not met requirements are changed", notMetRequirements);
|
||||||
|
Requirements requirements = requirementsWatcher.getRequirements();
|
||||||
|
for (Listener listener : listeners) {
|
||||||
|
listener.onRequirementsStateChanged(DownloadManager.this, requirements, notMetRequirements);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < downloads.size(); i++) {
|
||||||
|
downloads.get(i).setNotMetRequirements(notMetRequirements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) {
|
||||||
|
this.notMetRequirements = notMetRequirements;
|
||||||
|
if (notMetRequirements == 0) {
|
||||||
|
stopFlags &= ~STOP_FLAG_REQUIREMENTS_NOT_MET;
|
||||||
|
} else {
|
||||||
|
stopFlags |= STOP_FLAG_REQUIREMENTS_NOT_MET;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void loadActions() {
|
private void loadActions() {
|
||||||
fileIOHandler.post(
|
fileIOHandler.post(
|
||||||
() -> {
|
() -> {
|
||||||
@ -377,7 +451,9 @@ public final class DownloadManager {
|
|||||||
for (Listener listener : listeners) {
|
for (Listener listener : listeners) {
|
||||||
listener.onInitialized(DownloadManager.this);
|
listener.onInitialized(DownloadManager.this);
|
||||||
}
|
}
|
||||||
clearStopFlags(STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY);
|
for (int i = 0; i < downloads.size(); i++) {
|
||||||
|
downloads.get(i).start();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -420,37 +496,125 @@ public final class DownloadManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Requirements.RequirementFlags
|
||||||
|
private int watchRequirements(Requirements requirements) {
|
||||||
|
RequirementsWatcher.Listener listener =
|
||||||
|
(requirementsWatcher, notMetRequirements) -> onRequirementsStateChanged(notMetRequirements);
|
||||||
|
requirementsWatcher = new RequirementsWatcher(context, listener, requirements);
|
||||||
|
@Requirements.RequirementFlags int notMetRequirements = requirementsWatcher.start();
|
||||||
|
if (notMetRequirements == 0) {
|
||||||
|
startDownloads();
|
||||||
|
} else {
|
||||||
|
stopDownloads();
|
||||||
|
}
|
||||||
|
return notMetRequirements;
|
||||||
|
}
|
||||||
|
|
||||||
|
@StartThreadResults
|
||||||
|
private int startDownloadThread(Download download, DownloadAction action) {
|
||||||
|
if (!initialized || released) {
|
||||||
|
return START_THREAD_NOT_ALLOWED;
|
||||||
|
}
|
||||||
|
if (activeDownloads.containsKey(download)) {
|
||||||
|
if (stopDownloadThread(download)) {
|
||||||
|
return START_THREAD_WAIT_DOWNLOAD_CANCELLATION;
|
||||||
|
}
|
||||||
|
return START_THREAD_WAIT_REMOVAL_TO_FINISH;
|
||||||
|
}
|
||||||
|
if (!action.isRemoveAction) {
|
||||||
|
if (simultaneousDownloads == maxSimultaneousDownloads) {
|
||||||
|
return START_THREAD_TOO_MANY_DOWNLOADS;
|
||||||
|
}
|
||||||
|
simultaneousDownloads++;
|
||||||
|
}
|
||||||
|
Downloader downloader = downloaderFactory.createDownloader(action);
|
||||||
|
DownloadThread downloadThread = new DownloadThread(download, downloader, action.isRemoveAction);
|
||||||
|
activeDownloads.put(download, downloadThread);
|
||||||
|
logd("Download is started", download);
|
||||||
|
return START_THREAD_SUCCEEDED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean stopDownloadThread(Download download) {
|
||||||
|
DownloadThread downloadThread = activeDownloads.get(download);
|
||||||
|
if (downloadThread != null && !downloadThread.isRemoveThread) {
|
||||||
|
downloadThread.cancel();
|
||||||
|
logd("Download is cancelled", download);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopAllDownloadThreads() {
|
||||||
|
for (Download download : activeDownloads.keySet()) {
|
||||||
|
stopDownloadThread(download);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onDownloadThreadStopped(DownloadThread downloadThread, Throwable finalError) {
|
||||||
|
Download download = downloadThread.download;
|
||||||
|
logd("Download is stopped", download);
|
||||||
|
activeDownloads.remove(download);
|
||||||
|
boolean tryToStartDownloads = false;
|
||||||
|
if (!downloadThread.isRemoveThread) {
|
||||||
|
// If maxSimultaneousDownloads was hit, there might be a download waiting for a slot.
|
||||||
|
tryToStartDownloads = simultaneousDownloads == maxSimultaneousDownloads;
|
||||||
|
simultaneousDownloads--;
|
||||||
|
}
|
||||||
|
download.onDownloadThreadStopped(downloadThread.isCanceled, finalError);
|
||||||
|
if (tryToStartDownloads) {
|
||||||
|
for (int i = 0;
|
||||||
|
simultaneousDownloads < maxSimultaneousDownloads && i < downloads.size();
|
||||||
|
i++) {
|
||||||
|
downloads.get(i).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maybeNotifyListenersIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Downloader getDownloader(Download download) {
|
||||||
|
DownloadThread downloadThread = activeDownloads.get(download);
|
||||||
|
if (downloadThread != null) {
|
||||||
|
return downloadThread.downloader;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static final class Download {
|
private static final class Download {
|
||||||
|
|
||||||
private final String id;
|
private final String id;
|
||||||
private final DownloadManager downloadManager;
|
private final DownloadManager downloadManager;
|
||||||
private final DownloaderFactory downloaderFactory;
|
|
||||||
private final int minRetryCount;
|
|
||||||
private final long startTimeMs;
|
private final long startTimeMs;
|
||||||
private final ArrayDeque<DownloadAction> actionQueue;
|
private final ArrayDeque<DownloadAction> actionQueue;
|
||||||
/** The current state of the download. */
|
|
||||||
@DownloadState.State private int state;
|
|
||||||
|
|
||||||
@MonotonicNonNull private Downloader downloader;
|
@DownloadState.State private int state;
|
||||||
@MonotonicNonNull private DownloadThread downloadThread;
|
|
||||||
@MonotonicNonNull @DownloadState.FailureReason private int failureReason;
|
@MonotonicNonNull @DownloadState.FailureReason private int failureReason;
|
||||||
@DownloadState.StopFlags private int stopFlags;
|
@DownloadState.StopFlags private int stopFlags;
|
||||||
|
@Requirements.RequirementFlags private int notMetRequirements;
|
||||||
|
private int manualStopReason;
|
||||||
|
|
||||||
private Download(
|
private Download(
|
||||||
DownloadManager downloadManager,
|
DownloadManager downloadManager,
|
||||||
DownloaderFactory downloaderFactory,
|
|
||||||
DownloadAction action,
|
DownloadAction action,
|
||||||
int minRetryCount,
|
@DownloadState.StopFlags int stopFlags,
|
||||||
int stopFlags) {
|
@Requirements.RequirementFlags int notMetRequirements,
|
||||||
|
int manualStopReason) {
|
||||||
this.id = action.id;
|
this.id = action.id;
|
||||||
this.downloadManager = downloadManager;
|
this.downloadManager = downloadManager;
|
||||||
this.downloaderFactory = downloaderFactory;
|
this.notMetRequirements = notMetRequirements;
|
||||||
this.minRetryCount = minRetryCount;
|
this.manualStopReason = manualStopReason;
|
||||||
this.stopFlags = stopFlags;
|
this.stopFlags = stopFlags;
|
||||||
this.startTimeMs = System.currentTimeMillis();
|
this.startTimeMs = System.currentTimeMillis();
|
||||||
actionQueue = new ArrayDeque<>();
|
actionQueue = new ArrayDeque<>();
|
||||||
actionQueue.add(action);
|
actionQueue.add(action);
|
||||||
initialize(/* restart= */ false);
|
|
||||||
|
// Set to queued state but don't notify listeners until we make sure we don't switch to
|
||||||
|
// another state immediately.
|
||||||
|
state = STATE_QUEUED;
|
||||||
|
initialize();
|
||||||
|
if (state == STATE_QUEUED) {
|
||||||
|
downloadManager.onDownloadStateChange(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean addAction(DownloadAction newAction) {
|
public boolean addAction(DownloadAction newAction) {
|
||||||
@ -472,12 +636,9 @@ public final class DownloadManager {
|
|||||||
setState(STATE_REMOVING);
|
setState(STATE_REMOVING);
|
||||||
}
|
}
|
||||||
} else if (!action.equals(updatedAction)) {
|
} else if (!action.equals(updatedAction)) {
|
||||||
if (state == STATE_DOWNLOADING) {
|
Assertions.checkState(
|
||||||
stopDownloadThread();
|
state == STATE_DOWNLOADING || state == STATE_QUEUED || state == STATE_STOPPED);
|
||||||
} else {
|
initialize();
|
||||||
Assertions.checkState(state == STATE_QUEUED || state == STATE_STOPPED);
|
|
||||||
initialize(/* restart= */ false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -486,6 +647,7 @@ public final class DownloadManager {
|
|||||||
float downloadPercentage = C.PERCENTAGE_UNSET;
|
float downloadPercentage = C.PERCENTAGE_UNSET;
|
||||||
long downloadedBytes = 0;
|
long downloadedBytes = 0;
|
||||||
long totalBytes = C.LENGTH_UNSET;
|
long totalBytes = C.LENGTH_UNSET;
|
||||||
|
Downloader downloader = downloadManager.getDownloader(this);
|
||||||
if (downloader != null) {
|
if (downloader != null) {
|
||||||
downloadPercentage = downloader.getDownloadPercentage();
|
downloadPercentage = downloader.getDownloadPercentage();
|
||||||
downloadedBytes = downloader.getDownloadedBytes();
|
downloadedBytes = downloader.getDownloadedBytes();
|
||||||
@ -503,6 +665,8 @@ public final class DownloadManager {
|
|||||||
totalBytes,
|
totalBytes,
|
||||||
failureReason,
|
failureReason,
|
||||||
stopFlags,
|
stopFlags,
|
||||||
|
notMetRequirements,
|
||||||
|
manualStopReason,
|
||||||
startTimeMs,
|
startTimeMs,
|
||||||
/* updateTimeMs= */ System.currentTimeMillis(),
|
/* updateTimeMs= */ System.currentTimeMillis(),
|
||||||
action.keys.toArray(new StreamKey[0]),
|
action.keys.toArray(new StreamKey[0]),
|
||||||
@ -522,83 +686,89 @@ public final class DownloadManager {
|
|||||||
return id + ' ' + DownloadState.getStateString(state);
|
return id + ' ' + DownloadState.getStateString(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean start() {
|
public void start() {
|
||||||
if (state != STATE_QUEUED) {
|
if (state == STATE_QUEUED || state == STATE_DOWNLOADING) {
|
||||||
return false;
|
startOrQueue();
|
||||||
|
} else if (state == STATE_REMOVING || state == STATE_RESTARTING) {
|
||||||
|
downloadManager.startDownloadThread(this, actionQueue.peek());
|
||||||
}
|
}
|
||||||
startDownloadThread(actionQueue.peek());
|
|
||||||
setState(STATE_DOWNLOADING);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStopFlags(int flags) {
|
public void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) {
|
||||||
updateStopFlags(flags, flags);
|
this.notMetRequirements = notMetRequirements;
|
||||||
|
updateStopFlags(STOP_FLAG_REQUIREMENTS_NOT_MET, /* setFlags= */ notMetRequirements != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearStopFlags(int flags) {
|
public void setManualStopReason(int manualStopReason) {
|
||||||
updateStopFlags(flags, 0);
|
this.manualStopReason = manualStopReason;
|
||||||
|
updateStopFlags(STOP_FLAG_MANUAL, /* setFlags= */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateStopFlags(int flags, int values) {
|
public void clearManualStopReason() {
|
||||||
stopFlags = (values & flags) | (stopFlags & ~flags);
|
this.manualStopReason = 0;
|
||||||
|
updateStopFlags(STOP_FLAG_MANUAL, /* setFlags= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStopFlags(int flags, boolean setFlags) {
|
||||||
|
if (setFlags) {
|
||||||
|
stopFlags |= flags;
|
||||||
|
} else {
|
||||||
|
stopFlags &= ~flags;
|
||||||
|
}
|
||||||
if (stopFlags != 0) {
|
if (stopFlags != 0) {
|
||||||
if (state == STATE_DOWNLOADING) {
|
if (state == STATE_DOWNLOADING || state == STATE_QUEUED) {
|
||||||
stopDownloadThread();
|
downloadManager.stopDownloadThread(this);
|
||||||
} else if (state == STATE_QUEUED) {
|
|
||||||
setState(STATE_STOPPED);
|
setState(STATE_STOPPED);
|
||||||
}
|
}
|
||||||
} else if (state == STATE_STOPPED) {
|
} else if (state == STATE_STOPPED) {
|
||||||
startOrQueue(/* restart= */ false);
|
startOrQueue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initialize(boolean restart) {
|
private void initialize() {
|
||||||
DownloadAction action = actionQueue.peek();
|
DownloadAction action = actionQueue.peek();
|
||||||
if (action.isRemoveAction) {
|
if (action.isRemoveAction) {
|
||||||
if (!downloadManager.released) {
|
int result = downloadManager.startDownloadThread(this, action);
|
||||||
startDownloadThread(action);
|
Assertions.checkState(
|
||||||
}
|
result == START_THREAD_SUCCEEDED
|
||||||
|
|| result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION
|
||||||
|
|| result == START_THREAD_NOT_ALLOWED);
|
||||||
setState(actionQueue.size() == 1 ? STATE_REMOVING : STATE_RESTARTING);
|
setState(actionQueue.size() == 1 ? STATE_REMOVING : STATE_RESTARTING);
|
||||||
} else if (stopFlags != 0) {
|
} else if (stopFlags != 0) {
|
||||||
setState(STATE_STOPPED);
|
setState(STATE_STOPPED);
|
||||||
} else {
|
} else {
|
||||||
startOrQueue(restart);
|
startOrQueue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startOrQueue(boolean restart) {
|
private void startOrQueue() {
|
||||||
// Set to queued state but don't notify listeners until we make sure we can't start now.
|
DownloadAction action = Assertions.checkNotNull(actionQueue.peek());
|
||||||
state = STATE_QUEUED;
|
Assertions.checkState(!action.isRemoveAction);
|
||||||
if (restart) {
|
@StartThreadResults int result = downloadManager.startDownloadThread(this, action);
|
||||||
start();
|
Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH);
|
||||||
|
if (result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION) {
|
||||||
|
setState(STATE_DOWNLOADING);
|
||||||
} else {
|
} else {
|
||||||
downloadManager.maybeStartDownload(this);
|
setState(STATE_QUEUED);
|
||||||
}
|
|
||||||
if (state == STATE_QUEUED) {
|
|
||||||
downloadManager.onDownloadStateChange(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setState(@DownloadState.State int newState) {
|
private void setState(@DownloadState.State int newState) {
|
||||||
|
if (state != newState) {
|
||||||
state = newState;
|
state = newState;
|
||||||
downloadManager.onDownloadStateChange(this);
|
downloadManager.onDownloadStateChange(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startDownloadThread(DownloadAction action) {
|
|
||||||
downloader = downloaderFactory.createDownloader(action);
|
|
||||||
downloadThread =
|
|
||||||
new DownloadThread(
|
|
||||||
this, downloader, action.isRemoveAction, minRetryCount, downloadManager.handler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopDownloadThread() {
|
private void onDownloadThreadStopped(boolean isCanceled, @Nullable Throwable error) {
|
||||||
Assertions.checkNotNull(downloadThread).cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onDownloadThreadStopped(@Nullable Throwable finalError) {
|
|
||||||
failureReason = FAILURE_REASON_NONE;
|
failureReason = FAILURE_REASON_NONE;
|
||||||
if (!downloadThread.isCanceled) {
|
if (isCanceled) {
|
||||||
if (finalError != null && state != STATE_REMOVING && state != STATE_RESTARTING) {
|
if (!isIdle()) {
|
||||||
|
downloadManager.startDownloadThread(this, actionQueue.peek());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (error != null && state == STATE_DOWNLOADING) {
|
||||||
failureReason = FAILURE_REASON_UNKNOWN;
|
failureReason = FAILURE_REASON_UNKNOWN;
|
||||||
setState(STATE_FAILED);
|
setState(STATE_FAILED);
|
||||||
return;
|
return;
|
||||||
@ -613,32 +783,22 @@ public final class DownloadManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
actionQueue.remove();
|
actionQueue.remove();
|
||||||
}
|
initialize();
|
||||||
initialize(/* restart= */ state == STATE_DOWNLOADING);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class DownloadThread implements Runnable {
|
private class DownloadThread implements Runnable {
|
||||||
|
|
||||||
private final Download download;
|
private final Download download;
|
||||||
private final Downloader downloader;
|
private final Downloader downloader;
|
||||||
private final boolean remove;
|
private final boolean isRemoveThread;
|
||||||
private final int minRetryCount;
|
|
||||||
private final Handler callbackHandler;
|
|
||||||
private final Thread thread;
|
private final Thread thread;
|
||||||
private volatile boolean isCanceled;
|
private volatile boolean isCanceled;
|
||||||
|
|
||||||
private DownloadThread(
|
private DownloadThread(Download download, Downloader downloader, boolean isRemoveThread) {
|
||||||
Download download,
|
|
||||||
Downloader downloader,
|
|
||||||
boolean remove,
|
|
||||||
int minRetryCount,
|
|
||||||
Handler callbackHandler) {
|
|
||||||
this.download = download;
|
this.download = download;
|
||||||
this.downloader = downloader;
|
this.downloader = downloader;
|
||||||
this.remove = remove;
|
this.isRemoveThread = isRemoveThread;
|
||||||
this.minRetryCount = minRetryCount;
|
|
||||||
this.callbackHandler = callbackHandler;
|
|
||||||
thread = new Thread(this);
|
thread = new Thread(this);
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
@ -653,10 +813,10 @@ public final class DownloadManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
logd("Download is started", download);
|
logd("Download started", download);
|
||||||
Throwable error = null;
|
Throwable error = null;
|
||||||
try {
|
try {
|
||||||
if (remove) {
|
if (isRemoveThread) {
|
||||||
downloader.remove();
|
downloader.remove();
|
||||||
} else {
|
} else {
|
||||||
int errorCount = 0;
|
int errorCount = 0;
|
||||||
@ -686,11 +846,12 @@ public final class DownloadManager {
|
|||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
final Throwable finalError = error;
|
final Throwable finalError = error;
|
||||||
callbackHandler.post(() -> download.onDownloadThreadStopped(isCanceled ? null : finalError));
|
handler.post(() -> onDownloadThreadStopped(this, finalError));
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getRetryDelayMillis(int errorCount) {
|
private int getRetryDelayMillis(int errorCount) {
|
||||||
return Math.min((errorCount - 1) * 1000, 5000);
|
return Math.min((errorCount - 1) * 1000, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,8 @@ import android.os.Looper;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.StringRes;
|
import android.support.annotation.StringRes;
|
||||||
import com.google.android.exoplayer2.scheduler.Requirements;
|
import com.google.android.exoplayer2.scheduler.Requirements;
|
||||||
import com.google.android.exoplayer2.scheduler.RequirementsWatcher;
|
|
||||||
import com.google.android.exoplayer2.scheduler.Scheduler;
|
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.Log;
|
||||||
import com.google.android.exoplayer2.util.NotificationUtil;
|
import com.google.android.exoplayer2.util.NotificationUtil;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
@ -43,10 +43,6 @@ public abstract class DownloadService extends Service {
|
|||||||
/** Starts a download service, adding a new {@link DownloadAction} to be executed. */
|
/** Starts a download service, adding a new {@link DownloadAction} to be executed. */
|
||||||
public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD";
|
public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD";
|
||||||
|
|
||||||
/** Reloads the download requirements. */
|
|
||||||
public static final String ACTION_RELOAD_REQUIREMENTS =
|
|
||||||
"com.google.android.exoplayer.downloadService.action.RELOAD_REQUIREMENTS";
|
|
||||||
|
|
||||||
/** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */
|
/** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */
|
||||||
private static final String ACTION_RESTART =
|
private static final String ACTION_RESTART =
|
||||||
"com.google.android.exoplayer.downloadService.action.RESTART";
|
"com.google.android.exoplayer.downloadService.action.RESTART";
|
||||||
@ -70,20 +66,16 @@ public abstract class DownloadService extends Service {
|
|||||||
private static final String TAG = "DownloadService";
|
private static final String TAG = "DownloadService";
|
||||||
private static final boolean DEBUG = false;
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
// Keep the requirements helper for each DownloadService as long as there are downloads (and the
|
// Keep DownloadManagerListeners for each DownloadService as long as there are downloads (and the
|
||||||
// process is running). This allows downloads to resume when there's no scheduler. It may also
|
// process is running). This allows DownloadService to restart when there's no scheduler.
|
||||||
// allow downloads the resume more quickly than when relying on the scheduler alone.
|
private static final HashMap<Class<? extends DownloadService>, DownloadManagerHelper>
|
||||||
private static final HashMap<Class<? extends DownloadService>, RequirementsHelper>
|
downloadManagerListeners = new HashMap<>();
|
||||||
requirementsHelpers = new HashMap<>();
|
|
||||||
private static final Requirements DEFAULT_REQUIREMENTS =
|
|
||||||
new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
|
|
||||||
|
|
||||||
private final @Nullable ForegroundNotificationUpdater foregroundNotificationUpdater;
|
private final @Nullable ForegroundNotificationUpdater foregroundNotificationUpdater;
|
||||||
private final @Nullable String channelId;
|
private final @Nullable String channelId;
|
||||||
private final @StringRes int channelName;
|
private final @StringRes int channelName;
|
||||||
|
|
||||||
private DownloadManager downloadManager;
|
private DownloadManager downloadManager;
|
||||||
private DownloadManagerListener downloadManagerListener;
|
|
||||||
private int lastStartId;
|
private int lastStartId;
|
||||||
private boolean startedInForeground;
|
private boolean startedInForeground;
|
||||||
private boolean taskRemoved;
|
private boolean taskRemoved;
|
||||||
@ -227,9 +219,16 @@ public abstract class DownloadService extends Service {
|
|||||||
NotificationUtil.createNotificationChannel(
|
NotificationUtil.createNotificationChannel(
|
||||||
this, channelId, channelName, NotificationUtil.IMPORTANCE_LOW);
|
this, channelId, channelName, NotificationUtil.IMPORTANCE_LOW);
|
||||||
}
|
}
|
||||||
downloadManager = getDownloadManager();
|
Class<? extends DownloadService> clazz = getClass();
|
||||||
downloadManagerListener = new DownloadManagerListener();
|
DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz);
|
||||||
downloadManager.addListener(downloadManagerListener);
|
if (downloadManagerHelper == null) {
|
||||||
|
downloadManagerHelper =
|
||||||
|
new DownloadManagerHelper(
|
||||||
|
getApplicationContext(), getDownloadManager(), getScheduler(), clazz);
|
||||||
|
downloadManagerListeners.put(clazz, downloadManagerHelper);
|
||||||
|
}
|
||||||
|
downloadManager = downloadManagerHelper.downloadManager;
|
||||||
|
downloadManagerHelper.attachService(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -264,22 +263,11 @@ public abstract class DownloadService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ACTION_RELOAD_REQUIREMENTS:
|
|
||||||
stopWatchingRequirements();
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
Log.e(TAG, "Ignoring unrecognized action: " + intentAction);
|
Log.e(TAG, "Ignoring unrecognized action: " + intentAction);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Requirements requirements = getRequirements();
|
|
||||||
if (requirements.checkRequirements(this)) {
|
|
||||||
downloadManager.startDownloads();
|
|
||||||
} else {
|
|
||||||
downloadManager.stopDownloads();
|
|
||||||
}
|
|
||||||
maybeStartWatchingRequirements(requirements);
|
|
||||||
|
|
||||||
if (downloadManager.isIdle()) {
|
if (downloadManager.isIdle()) {
|
||||||
stop();
|
stop();
|
||||||
}
|
}
|
||||||
@ -295,11 +283,12 @@ public abstract class DownloadService extends Service {
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
logd("onDestroy");
|
logd("onDestroy");
|
||||||
|
DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(getClass());
|
||||||
|
boolean unschedule = downloadManager.getDownloadCount() <= 0;
|
||||||
|
downloadManagerHelper.detachService(this, unschedule);
|
||||||
if (foregroundNotificationUpdater != null) {
|
if (foregroundNotificationUpdater != null) {
|
||||||
foregroundNotificationUpdater.stopPeriodicUpdates();
|
foregroundNotificationUpdater.stopPeriodicUpdates();
|
||||||
}
|
}
|
||||||
downloadManager.removeListener(downloadManagerListener);
|
|
||||||
maybeStopWatchingRequirements();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** DownloadService isn't designed to be bound. */
|
/** DownloadService isn't designed to be bound. */
|
||||||
@ -311,9 +300,7 @@ public abstract class DownloadService extends Service {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the
|
* Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the
|
||||||
* life cycle of the service. The service will call {@link DownloadManager#startDownloads()} and
|
* life cycle of the process.
|
||||||
* {@link DownloadManager#stopDownloads} as necessary when requirements returned by {@link
|
|
||||||
* #getRequirements()} are met or stop being met.
|
|
||||||
*/
|
*/
|
||||||
protected abstract DownloadManager getDownloadManager();
|
protected abstract DownloadManager getDownloadManager();
|
||||||
|
|
||||||
@ -324,14 +311,6 @@ public abstract class DownloadService extends Service {
|
|||||||
*/
|
*/
|
||||||
protected abstract @Nullable Scheduler getScheduler();
|
protected abstract @Nullable Scheduler getScheduler();
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns requirements for downloads to take place. By default the only requirement is that the
|
|
||||||
* device has network connectivity.
|
|
||||||
*/
|
|
||||||
protected Requirements getRequirements() {
|
|
||||||
return DEFAULT_REQUIREMENTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should be overridden in the subclass if the service will be run in the foreground.
|
* Should be overridden in the subclass if the service will be run in the foreground.
|
||||||
*
|
*
|
||||||
@ -363,32 +342,16 @@ public abstract class DownloadService extends Service {
|
|||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeStartWatchingRequirements(Requirements requirements) {
|
private void notifyDownloadStateChange(DownloadState downloadState) {
|
||||||
if (downloadManager.getDownloadCount() == 0) {
|
onDownloadStateChanged(downloadState);
|
||||||
return;
|
if (foregroundNotificationUpdater != null) {
|
||||||
|
if (downloadState.state == DownloadState.STATE_DOWNLOADING
|
||||||
|
|| downloadState.state == DownloadState.STATE_REMOVING
|
||||||
|
|| downloadState.state == DownloadState.STATE_RESTARTING) {
|
||||||
|
foregroundNotificationUpdater.startPeriodicUpdates();
|
||||||
|
} else {
|
||||||
|
foregroundNotificationUpdater.update();
|
||||||
}
|
}
|
||||||
Class<? extends DownloadService> clazz = getClass();
|
|
||||||
RequirementsHelper requirementsHelper = requirementsHelpers.get(clazz);
|
|
||||||
if (requirementsHelper == null) {
|
|
||||||
requirementsHelper = new RequirementsHelper(this, requirements, getScheduler(), clazz);
|
|
||||||
requirementsHelpers.put(clazz, requirementsHelper);
|
|
||||||
requirementsHelper.start();
|
|
||||||
logd("started watching requirements");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeStopWatchingRequirements() {
|
|
||||||
if (downloadManager.getDownloadCount() > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stopWatchingRequirements();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopWatchingRequirements() {
|
|
||||||
RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass());
|
|
||||||
if (requirementsHelper != null) {
|
|
||||||
requirementsHelper.stop();
|
|
||||||
logd("stopped watching requirements");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -420,33 +383,6 @@ public abstract class DownloadService extends Service {
|
|||||||
return new Intent(context, clazz).setAction(action);
|
return new Intent(context, clazz).setAction(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class DownloadManagerListener implements DownloadManager.Listener {
|
|
||||||
@Override
|
|
||||||
public void onInitialized(DownloadManager downloadManager) {
|
|
||||||
maybeStartWatchingRequirements(getRequirements());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDownloadStateChanged(
|
|
||||||
DownloadManager downloadManager, DownloadState downloadState) {
|
|
||||||
DownloadService.this.onDownloadStateChanged(downloadState);
|
|
||||||
if (foregroundNotificationUpdater != null) {
|
|
||||||
if (downloadState.state == DownloadState.STATE_DOWNLOADING
|
|
||||||
|| downloadState.state == DownloadState.STATE_REMOVING
|
|
||||||
|| downloadState.state == DownloadState.STATE_RESTARTING) {
|
|
||||||
foregroundNotificationUpdater.startPeriodicUpdates();
|
|
||||||
} else {
|
|
||||||
foregroundNotificationUpdater.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final void onIdle(DownloadManager downloadManager) {
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class ForegroundNotificationUpdater implements Runnable {
|
private final class ForegroundNotificationUpdater implements Runnable {
|
||||||
|
|
||||||
private final int notificationId;
|
private final int notificationId;
|
||||||
@ -494,58 +430,87 @@ public abstract class DownloadService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class RequirementsHelper implements RequirementsWatcher.Listener {
|
private static final class DownloadManagerHelper implements DownloadManager.Listener {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final Requirements requirements;
|
private final DownloadManager downloadManager;
|
||||||
private final @Nullable Scheduler scheduler;
|
@Nullable private final Scheduler scheduler;
|
||||||
private final Class<? extends DownloadService> serviceClass;
|
private final Class<? extends DownloadService> serviceClass;
|
||||||
private final RequirementsWatcher requirementsWatcher;
|
@Nullable private DownloadService downloadService;
|
||||||
|
|
||||||
private RequirementsHelper(
|
private DownloadManagerHelper(
|
||||||
Context context,
|
Context context,
|
||||||
Requirements requirements,
|
DownloadManager downloadManager,
|
||||||
@Nullable Scheduler scheduler,
|
@Nullable Scheduler scheduler,
|
||||||
Class<? extends DownloadService> serviceClass) {
|
Class<? extends DownloadService> serviceClass) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.requirements = requirements;
|
this.downloadManager = downloadManager;
|
||||||
this.scheduler = scheduler;
|
this.scheduler = scheduler;
|
||||||
this.serviceClass = serviceClass;
|
this.serviceClass = serviceClass;
|
||||||
requirementsWatcher = new RequirementsWatcher(context, this, requirements);
|
downloadManager.addListener(this);
|
||||||
}
|
|
||||||
|
|
||||||
public void start() {
|
|
||||||
requirementsWatcher.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
requirementsWatcher.stop();
|
|
||||||
if (scheduler != null) {
|
if (scheduler != null) {
|
||||||
|
Requirements requirements = downloadManager.getRequirements();
|
||||||
|
setSchedulerEnabled(/* enabled= */ !requirements.checkRequirements(context), requirements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void attachService(DownloadService downloadService) {
|
||||||
|
Assertions.checkState(this.downloadService == null);
|
||||||
|
this.downloadService = downloadService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void detachService(DownloadService downloadService, boolean unschedule) {
|
||||||
|
Assertions.checkState(this.downloadService == downloadService);
|
||||||
|
this.downloadService = null;
|
||||||
|
if (scheduler != null && unschedule) {
|
||||||
scheduler.cancel();
|
scheduler.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requirementsMet(RequirementsWatcher requirementsWatcher) {
|
public void onInitialized(DownloadManager downloadManager) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDownloadStateChanged(
|
||||||
|
DownloadManager downloadManager, DownloadState downloadState) {
|
||||||
|
if (downloadService != null) {
|
||||||
|
downloadService.notifyDownloadStateChange(downloadState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onIdle(DownloadManager downloadManager) {
|
||||||
|
if (downloadService != null) {
|
||||||
|
downloadService.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequirementsStateChanged(
|
||||||
|
DownloadManager downloadManager,
|
||||||
|
Requirements requirements,
|
||||||
|
@Requirements.RequirementFlags int notMetRequirements) {
|
||||||
|
boolean requirementsMet = notMetRequirements == 0;
|
||||||
|
if (downloadService == null && requirementsMet) {
|
||||||
try {
|
try {
|
||||||
notifyService();
|
Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);
|
||||||
} catch (Exception e) {
|
context.startService(intent);
|
||||||
/* If we can't notify the service, don't stop the scheduler. */
|
} catch (IllegalStateException e) {
|
||||||
|
/* startService fails if the app is in the background then don't stop the scheduler. */
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (scheduler != null) {
|
if (scheduler != null) {
|
||||||
scheduler.cancel();
|
setSchedulerEnabled(/* enabled= */ !requirementsMet, requirements);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void setSchedulerEnabled(boolean enabled, Requirements requirements) {
|
||||||
public void requirementsNotMet(RequirementsWatcher requirementsWatcher) {
|
if (!enabled) {
|
||||||
try {
|
scheduler.cancel();
|
||||||
notifyService();
|
} else {
|
||||||
} catch (Exception e) {
|
|
||||||
/* Do nothing. The service isn't running anyway. */
|
|
||||||
}
|
|
||||||
if (scheduler != null) {
|
|
||||||
String servicePackage = context.getPackageName();
|
String servicePackage = context.getPackageName();
|
||||||
boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART);
|
boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@ -553,15 +518,5 @@ public abstract class DownloadService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyService() throws Exception {
|
|
||||||
Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);
|
|
||||||
try {
|
|
||||||
context.startService(intent);
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
/* startService will fail if the app is in the background and the service isn't running. */
|
|
||||||
throw new Exception(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,17 +13,20 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.google.android.exoplayer2.offline;
|
package com.google.android.exoplayer2.offline;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.scheduler.Requirements;
|
||||||
|
import com.google.android.exoplayer2.scheduler.Requirements.RequirementFlags;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
/** Represents state of a download. */
|
/** Represents state of a download. */
|
||||||
public final class DownloadState {
|
public final class DownloadState {
|
||||||
@ -74,19 +77,19 @@ public final class DownloadState {
|
|||||||
public static final int FAILURE_REASON_UNKNOWN = 1;
|
public static final int FAILURE_REASON_UNKNOWN = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download stop flags. Possible flag values are {@link #STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY} and
|
* Download stop flags. Possible flag values are {@link #STOP_FLAG_MANUAL} and {@link
|
||||||
* {@link #STOP_FLAG_STOPPED}.
|
* #STOP_FLAG_REQUIREMENTS_NOT_MET}.
|
||||||
*/
|
*/
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef(
|
@IntDef(
|
||||||
flag = true,
|
flag = true,
|
||||||
value = {STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY, STOP_FLAG_STOPPED})
|
value = {STOP_FLAG_MANUAL, STOP_FLAG_REQUIREMENTS_NOT_MET})
|
||||||
public @interface StopFlags {}
|
public @interface StopFlags {}
|
||||||
/** Download can't be started as the manager isn't ready. */
|
/** Download is stopped by the application. */
|
||||||
public static final int STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY = 1;
|
public static final int STOP_FLAG_MANUAL = 1;
|
||||||
/** All downloads are stopped by the application. */
|
/** Download is stopped as the requirements are not met. */
|
||||||
public static final int STOP_FLAG_STOPPED = 1 << 1;
|
public static final int STOP_FLAG_REQUIREMENTS_NOT_MET = 1 << 1;
|
||||||
|
|
||||||
/** Returns the state string for the given state value. */
|
/** Returns the state string for the given state value. */
|
||||||
public static String getStateString(@State int state) {
|
public static String getStateString(@State int state) {
|
||||||
@ -154,7 +157,40 @@ public final class DownloadState {
|
|||||||
*/
|
*/
|
||||||
@FailureReason public final int failureReason;
|
@FailureReason public final int failureReason;
|
||||||
/** Download stop flags. These flags stop downloading any content. */
|
/** Download stop flags. These flags stop downloading any content. */
|
||||||
public final int stopFlags;
|
@StopFlags public final int stopFlags;
|
||||||
|
/** Not met requirements to download. */
|
||||||
|
@Requirements.RequirementFlags public final int notMetRequirements;
|
||||||
|
/** If {@link #STOP_FLAG_MANUAL} is set then this field holds the manual stop reason. */
|
||||||
|
public final int manualStopReason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DownloadState} using a {@link DownloadAction}.
|
||||||
|
*
|
||||||
|
* @param action The {@link DownloadAction}.
|
||||||
|
*/
|
||||||
|
public DownloadState(DownloadAction action) {
|
||||||
|
this(action, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DownloadState(DownloadAction action, long currentTimeMs) {
|
||||||
|
this(
|
||||||
|
action.id,
|
||||||
|
action.type,
|
||||||
|
action.uri,
|
||||||
|
action.customCacheKey,
|
||||||
|
/* state= */ action.isRemoveAction ? STATE_REMOVING : STATE_QUEUED,
|
||||||
|
/* downloadPercentage= */ C.PERCENTAGE_UNSET,
|
||||||
|
/* downloadedBytes= */ 0,
|
||||||
|
/* totalBytes= */ C.LENGTH_UNSET,
|
||||||
|
FAILURE_REASON_NONE,
|
||||||
|
/* stopFlags= */ 0,
|
||||||
|
/* notMetRequirements= */ 0,
|
||||||
|
/* manualStopReason= */ 0,
|
||||||
|
/* startTimeMs= */ currentTimeMs,
|
||||||
|
/* updateTimeMs= */ currentTimeMs,
|
||||||
|
action.keys.toArray(new StreamKey[0]),
|
||||||
|
action.data);
|
||||||
|
}
|
||||||
|
|
||||||
/* package */ DownloadState(
|
/* package */ DownloadState(
|
||||||
String id,
|
String id,
|
||||||
@ -167,28 +203,91 @@ public final class DownloadState {
|
|||||||
long totalBytes,
|
long totalBytes,
|
||||||
@FailureReason int failureReason,
|
@FailureReason int failureReason,
|
||||||
@StopFlags int stopFlags,
|
@StopFlags int stopFlags,
|
||||||
|
@RequirementFlags int notMetRequirements,
|
||||||
|
int manualStopReason,
|
||||||
long startTimeMs,
|
long startTimeMs,
|
||||||
long updateTimeMs,
|
long updateTimeMs,
|
||||||
StreamKey[] streamKeys,
|
StreamKey[] streamKeys,
|
||||||
byte[] customMetadata) {
|
byte[] customMetadata) {
|
||||||
this.stopFlags = stopFlags;
|
Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED));
|
||||||
|
Assertions.checkState(stopFlags == 0 || (state != STATE_DOWNLOADING && state != STATE_QUEUED));
|
||||||
Assertions.checkState(
|
Assertions.checkState(
|
||||||
failureReason == FAILURE_REASON_NONE ? state != STATE_FAILED : state == STATE_FAILED);
|
((stopFlags & STOP_FLAG_REQUIREMENTS_NOT_MET) == 0) == (notMetRequirements == 0));
|
||||||
// TODO enable this when we start changing state immediately
|
Assertions.checkState(((stopFlags & STOP_FLAG_MANUAL) != 0) || (manualStopReason == 0));
|
||||||
// Assertions.checkState(stopFlags == 0 || (state != STATE_DOWNLOADING && state !=
|
|
||||||
// STATE_QUEUED));
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.cacheKey = cacheKey;
|
this.cacheKey = cacheKey;
|
||||||
this.streamKeys = streamKeys;
|
|
||||||
this.customMetadata = customMetadata;
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.downloadPercentage = downloadPercentage;
|
this.downloadPercentage = downloadPercentage;
|
||||||
this.downloadedBytes = downloadedBytes;
|
this.downloadedBytes = downloadedBytes;
|
||||||
this.totalBytes = totalBytes;
|
this.totalBytes = totalBytes;
|
||||||
this.failureReason = failureReason;
|
this.failureReason = failureReason;
|
||||||
|
this.stopFlags = stopFlags;
|
||||||
|
this.notMetRequirements = notMetRequirements;
|
||||||
|
this.manualStopReason = manualStopReason;
|
||||||
this.startTimeMs = startTimeMs;
|
this.startTimeMs = startTimeMs;
|
||||||
this.updateTimeMs = updateTimeMs;
|
this.updateTimeMs = updateTimeMs;
|
||||||
|
this.streamKeys = streamKeys;
|
||||||
|
this.customMetadata = customMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges the given {@link DownloadAction} and creates a new {@link DownloadState}. The action
|
||||||
|
* must have the same id and type.
|
||||||
|
*
|
||||||
|
* @param action The {@link DownloadAction} to be merged.
|
||||||
|
* @return A new {@link DownloadState}.
|
||||||
|
*/
|
||||||
|
public DownloadState mergeAction(DownloadAction action) {
|
||||||
|
Assertions.checkArgument(action.id.equals(id));
|
||||||
|
Assertions.checkArgument(action.type.equals(type));
|
||||||
|
return new DownloadState(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
action.uri,
|
||||||
|
action.customCacheKey,
|
||||||
|
getNextState(action, state),
|
||||||
|
/* downloadPercentage= */ C.PERCENTAGE_UNSET,
|
||||||
|
downloadedBytes,
|
||||||
|
/* totalBytes= */ C.LENGTH_UNSET,
|
||||||
|
FAILURE_REASON_NONE,
|
||||||
|
stopFlags,
|
||||||
|
notMetRequirements,
|
||||||
|
manualStopReason,
|
||||||
|
startTimeMs,
|
||||||
|
updateTimeMs,
|
||||||
|
mergeStreamKeys(this, action),
|
||||||
|
action.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getNextState(DownloadAction action, int currentState) {
|
||||||
|
int newState;
|
||||||
|
if (action.isRemoveAction) {
|
||||||
|
newState = STATE_REMOVING;
|
||||||
|
} else {
|
||||||
|
if (currentState == STATE_REMOVING || currentState == STATE_RESTARTING) {
|
||||||
|
newState = STATE_RESTARTING;
|
||||||
|
} else if (currentState == STATE_STOPPED) {
|
||||||
|
newState = STATE_STOPPED;
|
||||||
|
} else {
|
||||||
|
newState = STATE_QUEUED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StreamKey[] mergeStreamKeys(DownloadState downloadState, DownloadAction action) {
|
||||||
|
StreamKey[] streamKeys = downloadState.streamKeys;
|
||||||
|
if (!action.isRemoveAction && streamKeys.length > 0) {
|
||||||
|
if (action.keys.isEmpty()) {
|
||||||
|
streamKeys = new StreamKey[0];
|
||||||
|
} else {
|
||||||
|
HashSet<StreamKey> keys = new HashSet<>(action.keys);
|
||||||
|
Collections.addAll(keys, downloadState.streamKeys);
|
||||||
|
streamKeys = keys.toArray(new StreamKey[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return streamKeys;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.offline;
|
||||||
|
|
||||||
|
/** Provides random read-write access to the result set returned by a database query. */
|
||||||
|
public interface DownloadStateCursor {
|
||||||
|
|
||||||
|
/** Returns the DownloadState at the current position. */
|
||||||
|
DownloadState getDownloadState();
|
||||||
|
|
||||||
|
/** Returns the numbers of DownloadStates in the cursor. */
|
||||||
|
int getCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current position of the cursor in the DownloadState set. The value is zero-based.
|
||||||
|
* When the DownloadState set is first returned the cursor will be at positon -1, which is before
|
||||||
|
* the first DownloadState. After the last DownloadState is returned another call to next() will
|
||||||
|
* leave the cursor past the last entry, at a position of count().
|
||||||
|
*
|
||||||
|
* @return the current cursor position.
|
||||||
|
*/
|
||||||
|
int getPosition();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor to an absolute position. The valid range of values is -1 <= position <=
|
||||||
|
* count.
|
||||||
|
*
|
||||||
|
* <p>This method will return true if the request destination was reachable, otherwise, it returns
|
||||||
|
* false.
|
||||||
|
*
|
||||||
|
* @param position the zero-based position to move to.
|
||||||
|
* @return whether the requested move fully succeeded.
|
||||||
|
*/
|
||||||
|
boolean moveToPosition(int position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor to the first DownloadState.
|
||||||
|
*
|
||||||
|
* <p>This method will return false if the cursor is empty.
|
||||||
|
*
|
||||||
|
* @return whether the move succeeded.
|
||||||
|
*/
|
||||||
|
default boolean moveToFirst() {
|
||||||
|
return moveToPosition(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor to the last DownloadState.
|
||||||
|
*
|
||||||
|
* <p>This method will return false if the cursor is empty.
|
||||||
|
*
|
||||||
|
* @return whether the move succeeded.
|
||||||
|
*/
|
||||||
|
default boolean moveToLast() {
|
||||||
|
return moveToPosition(getCount() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor to the next DownloadState.
|
||||||
|
*
|
||||||
|
* <p>This method will return false if the cursor is already past the last entry in the result
|
||||||
|
* set.
|
||||||
|
*
|
||||||
|
* @return whether the move succeeded.
|
||||||
|
*/
|
||||||
|
default boolean moveToNext() {
|
||||||
|
return moveToPosition(getPosition() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the cursor to the previous DownloadState.
|
||||||
|
*
|
||||||
|
* <p>This method will return false if the cursor is already before the first entry in the result
|
||||||
|
* set.
|
||||||
|
*
|
||||||
|
* @return whether the move succeeded.
|
||||||
|
*/
|
||||||
|
default boolean moveToPrevious() {
|
||||||
|
return moveToPosition(getPosition() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether the cursor is pointing to the first DownloadState. */
|
||||||
|
default boolean isFirst() {
|
||||||
|
return getPosition() == 0 && getCount() != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether the cursor is pointing to the last DownloadState. */
|
||||||
|
default boolean isLast() {
|
||||||
|
int count = getCount();
|
||||||
|
return getPosition() == (count - 1) && count != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether the cursor is pointing to the position before the first DownloadState. */
|
||||||
|
default boolean isBeforeFirst() {
|
||||||
|
if (getCount() == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return getPosition() == -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns whether the cursor is pointing to the position after the last DownloadState. */
|
||||||
|
default boolean isAfterLast() {
|
||||||
|
if (getCount() == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return getPosition() == getCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Closes the Cursor, releasing all of its resources and making it completely invalid. */
|
||||||
|
void close();
|
||||||
|
|
||||||
|
/** Returns whether the cursor is closed */
|
||||||
|
boolean isClosed();
|
||||||
|
}
|
@ -109,16 +109,16 @@ public final class DownloaderConstructorHelper {
|
|||||||
cacheReadDataSourceFactory != null
|
cacheReadDataSourceFactory != null
|
||||||
? cacheReadDataSourceFactory
|
? cacheReadDataSourceFactory
|
||||||
: new FileDataSourceFactory();
|
: new FileDataSourceFactory();
|
||||||
DataSink.Factory writeDataSinkFactory =
|
if (cacheWriteDataSinkFactory == null) {
|
||||||
cacheWriteDataSinkFactory != null
|
cacheWriteDataSinkFactory =
|
||||||
? cacheWriteDataSinkFactory
|
new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE);
|
||||||
: new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_MAX_CACHE_FILE_SIZE);
|
}
|
||||||
onlineCacheDataSourceFactory =
|
onlineCacheDataSourceFactory =
|
||||||
new CacheDataSourceFactory(
|
new CacheDataSourceFactory(
|
||||||
cache,
|
cache,
|
||||||
upstreamFactory,
|
upstreamFactory,
|
||||||
readDataSourceFactory,
|
readDataSourceFactory,
|
||||||
writeDataSinkFactory,
|
cacheWriteDataSinkFactory,
|
||||||
CacheDataSource.FLAG_BLOCK_ON_CACHE,
|
CacheDataSource.FLAG_BLOCK_ON_CACHE,
|
||||||
/* eventListener= */ null,
|
/* eventListener= */ null,
|
||||||
cacheKeyFactory);
|
cacheKeyFactory);
|
||||||
|
@ -1,66 +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.offline;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import com.google.android.exoplayer2.Renderer;
|
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
|
||||||
|
|
||||||
/** A {@link DownloadHelper} for progressive streams. */
|
|
||||||
public final class ProgressiveDownloadHelper extends DownloadHelper<Void> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates download helper for progressive streams.
|
|
||||||
*
|
|
||||||
* @param uri The stream {@link Uri}.
|
|
||||||
*/
|
|
||||||
public ProgressiveDownloadHelper(Uri uri) {
|
|
||||||
this(uri, /* cacheKey= */ null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates download helper for progressive streams.
|
|
||||||
*
|
|
||||||
* @param uri The stream {@link Uri}.
|
|
||||||
* @param cacheKey An optional cache key.
|
|
||||||
*/
|
|
||||||
public ProgressiveDownloadHelper(Uri uri, @Nullable String cacheKey) {
|
|
||||||
super(
|
|
||||||
DownloadAction.TYPE_PROGRESSIVE,
|
|
||||||
uri,
|
|
||||||
cacheKey,
|
|
||||||
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
|
|
||||||
(handler, videoListener, audioListener, metadata, text, drm) -> new Renderer[0],
|
|
||||||
/* drmSessionManager= */ null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void loadManifest(Uri uri) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected TrackGroupArray[] getTrackGroupArrays(Void manifest) {
|
|
||||||
return new TrackGroupArray[] {TrackGroupArray.EMPTY};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected StreamKey toStreamKey(
|
|
||||||
int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
|
|
||||||
return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup);
|
|
||||||
}
|
|
||||||
}
|
|
@ -53,7 +53,11 @@ public final class ProgressiveDownloader implements Downloader {
|
|||||||
Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) {
|
Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) {
|
||||||
this.dataSpec =
|
this.dataSpec =
|
||||||
new DataSpec(
|
new DataSpec(
|
||||||
uri, /* absoluteStreamPosition= */ 0, C.LENGTH_UNSET, customCacheKey, /* flags= */ 0);
|
uri,
|
||||||
|
/* absoluteStreamPosition= */ 0,
|
||||||
|
C.LENGTH_UNSET,
|
||||||
|
customCacheKey,
|
||||||
|
/* flags= */ DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);
|
||||||
this.cache = constructorHelper.getCache();
|
this.cache = constructorHelper.getCache();
|
||||||
this.dataSource = constructorHelper.createCacheDataSource();
|
this.dataSource = constructorHelper.createCacheDataSource();
|
||||||
this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();
|
this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();
|
||||||
|
@ -155,16 +155,14 @@ public final class Requirements {
|
|||||||
* @return Whether the requirements are met.
|
* @return Whether the requirements are met.
|
||||||
*/
|
*/
|
||||||
public boolean checkRequirements(Context context) {
|
public boolean checkRequirements(Context context) {
|
||||||
return checkNetworkRequirements(context)
|
return getNotMetRequirements(context) == 0;
|
||||||
&& checkChargingRequirement(context)
|
|
||||||
&& checkIdleRequirement(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the requirement flags that are not met, or 0.
|
* Returns {@link RequirementFlags} that are not met, or 0.
|
||||||
*
|
*
|
||||||
* @param context Any context.
|
* @param context Any context.
|
||||||
* @return The requirement flags that are not met, or 0.
|
* @return RequirementFlags that are not met, or 0.
|
||||||
*/
|
*/
|
||||||
@RequirementFlags
|
@RequirementFlags
|
||||||
public int getNotMetRequirements(Context context) {
|
public int getNotMetRequirements(Context context) {
|
||||||
@ -202,7 +200,7 @@ public final class Requirements {
|
|||||||
logd("Roaming: " + roaming);
|
logd("Roaming: " + roaming);
|
||||||
return !roaming;
|
return !roaming;
|
||||||
}
|
}
|
||||||
boolean activeNetworkMetered = isActiveNetworkMetered(connectivityManager, networkInfo);
|
boolean activeNetworkMetered = connectivityManager.isActiveNetworkMetered();
|
||||||
logd("Metered network: " + activeNetworkMetered);
|
logd("Metered network: " + activeNetworkMetered);
|
||||||
if (networkRequirement == NETWORK_TYPE_UNMETERED) {
|
if (networkRequirement == NETWORK_TYPE_UNMETERED) {
|
||||||
return !activeNetworkMetered;
|
return !activeNetworkMetered;
|
||||||
@ -257,17 +255,6 @@ public final class Requirements {
|
|||||||
return !validated;
|
return !validated;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isActiveNetworkMetered(
|
|
||||||
ConnectivityManager connectivityManager, NetworkInfo networkInfo) {
|
|
||||||
if (Util.SDK_INT >= 16) {
|
|
||||||
return connectivityManager.isActiveNetworkMetered();
|
|
||||||
}
|
|
||||||
int type = networkInfo.getType();
|
|
||||||
return type != ConnectivityManager.TYPE_WIFI
|
|
||||||
&& type != ConnectivityManager.TYPE_BLUETOOTH
|
|
||||||
&& type != ConnectivityManager.TYPE_ETHERNET;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void logd(String message) {
|
private static void logd(String message) {
|
||||||
if (Scheduler.DEBUG) {
|
if (Scheduler.DEBUG) {
|
||||||
Log.d(TAG, message);
|
Log.d(TAG, message);
|
||||||
@ -285,4 +272,20 @@ public final class Requirements {
|
|||||||
+ (isIdleRequired() ? ",idle" : "")
|
+ (isIdleRequired() ? ",idle" : "")
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return requirements == ((Requirements) o).requirements;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return requirements;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,21 +42,16 @@ public final class RequirementsWatcher {
|
|||||||
* Requirements} are met.
|
* Requirements} are met.
|
||||||
*/
|
*/
|
||||||
public interface Listener {
|
public interface Listener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when all of the requirements are met.
|
* Called when there is a change on the met requirements.
|
||||||
*
|
*
|
||||||
* @param requirementsWatcher Calling instance.
|
* @param requirementsWatcher Calling instance.
|
||||||
|
* @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not
|
||||||
|
* met, or 0.
|
||||||
*/
|
*/
|
||||||
void requirementsMet(RequirementsWatcher requirementsWatcher);
|
void onRequirementsStateChanged(
|
||||||
|
RequirementsWatcher requirementsWatcher,
|
||||||
/**
|
@Requirements.RequirementFlags int notMetRequirements);
|
||||||
* Called when there is at least one not met requirement and there is a change on which of the
|
|
||||||
* requirements are not met.
|
|
||||||
*
|
|
||||||
* @param requirementsWatcher Calling instance.
|
|
||||||
*/
|
|
||||||
void requirementsNotMet(RequirementsWatcher requirementsWatcher);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String TAG = "RequirementsWatcher";
|
private static final String TAG = "RequirementsWatcher";
|
||||||
@ -66,8 +61,9 @@ public final class RequirementsWatcher {
|
|||||||
private final Requirements requirements;
|
private final Requirements requirements;
|
||||||
private DeviceStatusChangeReceiver receiver;
|
private DeviceStatusChangeReceiver receiver;
|
||||||
|
|
||||||
private int notMetRequirements;
|
@Requirements.RequirementFlags private int notMetRequirements;
|
||||||
private CapabilityValidatedCallback networkCallback;
|
private CapabilityValidatedCallback networkCallback;
|
||||||
|
private Handler handler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param context Any context.
|
* @param context Any context.
|
||||||
@ -84,9 +80,13 @@ public final class RequirementsWatcher {
|
|||||||
/**
|
/**
|
||||||
* Starts watching for changes. Must be called from a thread that has an associated {@link
|
* Starts watching for changes. Must be called from a thread that has an associated {@link
|
||||||
* Looper}. Listener methods are called on the caller thread.
|
* Looper}. Listener methods are called on the caller thread.
|
||||||
|
*
|
||||||
|
* @return Initial {@link Requirements.RequirementFlags RequirementFlags} that are not met, or 0.
|
||||||
*/
|
*/
|
||||||
public void start() {
|
@Requirements.RequirementFlags
|
||||||
|
public int start() {
|
||||||
Assertions.checkNotNull(Looper.myLooper());
|
Assertions.checkNotNull(Looper.myLooper());
|
||||||
|
handler = new Handler();
|
||||||
|
|
||||||
notMetRequirements = requirements.getNotMetRequirements(context);
|
notMetRequirements = requirements.getNotMetRequirements(context);
|
||||||
|
|
||||||
@ -111,8 +111,9 @@ public final class RequirementsWatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
receiver = new DeviceStatusChangeReceiver();
|
receiver = new DeviceStatusChangeReceiver();
|
||||||
context.registerReceiver(receiver, filter, null, new Handler());
|
context.registerReceiver(receiver, filter, null, handler);
|
||||||
logd(this + " started");
|
logd(this + " started");
|
||||||
|
return notMetRequirements;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stops watching for changes. */
|
/** Stops watching for changes. */
|
||||||
@ -160,18 +161,12 @@ public final class RequirementsWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkRequirements() {
|
private void checkRequirements() {
|
||||||
|
@Requirements.RequirementFlags
|
||||||
int notMetRequirements = requirements.getNotMetRequirements(context);
|
int notMetRequirements = requirements.getNotMetRequirements(context);
|
||||||
if (this.notMetRequirements == notMetRequirements) {
|
if (this.notMetRequirements != notMetRequirements) {
|
||||||
logd("notMetRequirements hasn't changed: " + notMetRequirements);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.notMetRequirements = notMetRequirements;
|
this.notMetRequirements = notMetRequirements;
|
||||||
if (notMetRequirements == 0) {
|
logd("notMetRequirements has changed: " + notMetRequirements);
|
||||||
logd("start job");
|
listener.onRequirementsStateChanged(this, notMetRequirements);
|
||||||
listener.requirementsMet(this);
|
|
||||||
} else {
|
|
||||||
logd("stop job");
|
|
||||||
listener.requirementsNotMet(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,16 +190,22 @@ public final class RequirementsWatcher {
|
|||||||
private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback {
|
private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback {
|
||||||
@Override
|
@Override
|
||||||
public void onAvailable(Network network) {
|
public void onAvailable(Network network) {
|
||||||
super.onAvailable(network);
|
onNetworkCallback();
|
||||||
logd(RequirementsWatcher.this + " NetworkCallback.onAvailable");
|
|
||||||
checkRequirements();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLost(Network network) {
|
public void onLost(Network network) {
|
||||||
super.onLost(network);
|
onNetworkCallback();
|
||||||
logd(RequirementsWatcher.this + " NetworkCallback.onLost");
|
}
|
||||||
|
|
||||||
|
private void onNetworkCallback() {
|
||||||
|
handler.post(
|
||||||
|
() -> {
|
||||||
|
if (networkCallback != null) {
|
||||||
|
logd(RequirementsWatcher.this + " NetworkCallback");
|
||||||
checkRequirements();
|
checkRequirements();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,10 +206,10 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||||
ClippingMediaPeriod mediaPeriod =
|
ClippingMediaPeriod mediaPeriod =
|
||||||
new ClippingMediaPeriod(
|
new ClippingMediaPeriod(
|
||||||
mediaSource.createPeriod(id, allocator),
|
mediaSource.createPeriod(id, allocator, startPositionUs),
|
||||||
enableInitialDiscontinuity,
|
enableInitialDiscontinuity,
|
||||||
periodStartUs,
|
periodStartUs,
|
||||||
periodEndUs);
|
periodEndUs);
|
||||||
|
@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
|
|||||||
import com.google.android.exoplayer2.upstream.Allocator;
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.EventDispatcher;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -36,9 +35,11 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
|
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
|
||||||
@ -51,12 +52,19 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
private static final int MSG_REMOVE = 1;
|
private static final int MSG_REMOVE = 1;
|
||||||
private static final int MSG_MOVE = 2;
|
private static final int MSG_MOVE = 2;
|
||||||
private static final int MSG_SET_SHUFFLE_ORDER = 3;
|
private static final int MSG_SET_SHUFFLE_ORDER = 3;
|
||||||
private static final int MSG_NOTIFY_LISTENER = 4;
|
private static final int MSG_UPDATE_TIMELINE = 4;
|
||||||
private static final int MSG_ON_COMPLETION = 5;
|
private static final int MSG_ON_COMPLETION = 5;
|
||||||
|
|
||||||
// Accessed on any thread.
|
// Accessed on any thread.
|
||||||
|
@GuardedBy("this")
|
||||||
private final List<MediaSourceHolder> mediaSourcesPublic;
|
private final List<MediaSourceHolder> mediaSourcesPublic;
|
||||||
@Nullable private Handler playbackThreadHandler;
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
private final Set<HandlerAndRunnable> pendingOnCompletionActions;
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
@Nullable
|
||||||
|
private Handler playbackThreadHandler;
|
||||||
|
|
||||||
// Accessed on the playback thread only.
|
// Accessed on the playback thread only.
|
||||||
private final List<MediaSourceHolder> mediaSourceHolders;
|
private final List<MediaSourceHolder> mediaSourceHolders;
|
||||||
@ -67,8 +75,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
private final Timeline.Window window;
|
private final Timeline.Window window;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
|
|
||||||
private boolean listenerNotificationScheduled;
|
private boolean timelineUpdateScheduled;
|
||||||
private EventDispatcher<Runnable> pendingOnCompletionActions;
|
private Set<HandlerAndRunnable> nextTimelineUpdateOnCompletionActions;
|
||||||
private ShuffleOrder shuffleOrder;
|
private ShuffleOrder shuffleOrder;
|
||||||
private int windowCount;
|
private int windowCount;
|
||||||
private int periodCount;
|
private int periodCount;
|
||||||
@ -127,7 +135,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
this.mediaSourceByUid = new HashMap<>();
|
this.mediaSourceByUid = new HashMap<>();
|
||||||
this.mediaSourcesPublic = new ArrayList<>();
|
this.mediaSourcesPublic = new ArrayList<>();
|
||||||
this.mediaSourceHolders = new ArrayList<>();
|
this.mediaSourceHolders = new ArrayList<>();
|
||||||
this.pendingOnCompletionActions = new EventDispatcher<>();
|
this.nextTimelineUpdateOnCompletionActions = new HashSet<>();
|
||||||
|
this.pendingOnCompletionActions = new HashSet<>();
|
||||||
this.isAtomic = isAtomic;
|
this.isAtomic = isAtomic;
|
||||||
this.useLazyPreparation = useLazyPreparation;
|
this.useLazyPreparation = useLazyPreparation;
|
||||||
window = new Timeline.Window();
|
window = new Timeline.Window();
|
||||||
@ -148,13 +157,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
* Appends a {@link MediaSource} to the playlist and executes a custom action on completion.
|
* Appends a {@link MediaSource} to the playlist and executes a custom action on completion.
|
||||||
*
|
*
|
||||||
* @param mediaSource The {@link MediaSource} to be added to the list.
|
* @param mediaSource The {@link MediaSource} to be added to the list.
|
||||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||||
* source has been added to the playlist.
|
* source has been added to the playlist.
|
||||||
*/
|
*/
|
||||||
public final synchronized void addMediaSource(
|
public final synchronized void addMediaSource(
|
||||||
MediaSource mediaSource, Handler handler, Runnable actionOnCompletion) {
|
MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
|
||||||
addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, actionOnCompletion);
|
addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, onCompletionAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -169,7 +178,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
index,
|
index,
|
||||||
Collections.singletonList(mediaSource),
|
Collections.singletonList(mediaSource),
|
||||||
/* handler= */ null,
|
/* handler= */ null,
|
||||||
/* actionOnCompletion= */ null);
|
/* onCompletionAction= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -178,14 +187,14 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
* @param index The index at which the new {@link MediaSource} will be inserted. This index must
|
* @param index The index at which the new {@link MediaSource} will be inserted. This index must
|
||||||
* be in the range of 0 <= index <= {@link #getSize()}.
|
* be in the range of 0 <= index <= {@link #getSize()}.
|
||||||
* @param mediaSource The {@link MediaSource} to be added to the list.
|
* @param mediaSource The {@link MediaSource} to be added to the list.
|
||||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||||
* source has been added to the playlist.
|
* source has been added to the playlist.
|
||||||
*/
|
*/
|
||||||
public final synchronized void addMediaSource(
|
public final synchronized void addMediaSource(
|
||||||
int index, MediaSource mediaSource, Handler handler, Runnable actionOnCompletion) {
|
int index, MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
|
||||||
addPublicMediaSources(
|
addPublicMediaSources(
|
||||||
index, Collections.singletonList(mediaSource), handler, actionOnCompletion);
|
index, Collections.singletonList(mediaSource), handler, onCompletionAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -199,7 +208,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
mediaSourcesPublic.size(),
|
mediaSourcesPublic.size(),
|
||||||
mediaSources,
|
mediaSources,
|
||||||
/* handler= */ null,
|
/* handler= */ null,
|
||||||
/* actionOnCompletion= */ null);
|
/* onCompletionAction= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -208,13 +217,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
*
|
*
|
||||||
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
|
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
|
||||||
* sources are added in the order in which they appear in this collection.
|
* sources are added in the order in which they appear in this collection.
|
||||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||||
* sources have been added to the playlist.
|
* sources have been added to the playlist.
|
||||||
*/
|
*/
|
||||||
public final synchronized void addMediaSources(
|
public final synchronized void addMediaSources(
|
||||||
Collection<MediaSource> mediaSources, Handler handler, Runnable actionOnCompletion) {
|
Collection<MediaSource> mediaSources, Handler handler, Runnable onCompletionAction) {
|
||||||
addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, actionOnCompletion);
|
addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -226,7 +235,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
* sources are added in the order in which they appear in this collection.
|
* sources are added in the order in which they appear in this collection.
|
||||||
*/
|
*/
|
||||||
public final synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) {
|
public final synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) {
|
||||||
addPublicMediaSources(index, mediaSources, /* handler= */ null, /* actionOnCompletion= */ null);
|
addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -236,16 +245,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
* be in the range of 0 <= index <= {@link #getSize()}.
|
* be in the range of 0 <= index <= {@link #getSize()}.
|
||||||
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
|
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
|
||||||
* sources are added in the order in which they appear in this collection.
|
* sources are added in the order in which they appear in this collection.
|
||||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||||
* sources have been added to the playlist.
|
* sources have been added to the playlist.
|
||||||
*/
|
*/
|
||||||
public final synchronized void addMediaSources(
|
public final synchronized void addMediaSources(
|
||||||
int index,
|
int index,
|
||||||
Collection<MediaSource> mediaSources,
|
Collection<MediaSource> mediaSources,
|
||||||
Handler handler,
|
Handler handler,
|
||||||
Runnable actionOnCompletion) {
|
Runnable onCompletionAction) {
|
||||||
addPublicMediaSources(index, mediaSources, handler, actionOnCompletion);
|
addPublicMediaSources(index, mediaSources, handler, onCompletionAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,7 +270,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
* range of 0 <= index < {@link #getSize()}.
|
* range of 0 <= index < {@link #getSize()}.
|
||||||
*/
|
*/
|
||||||
public final synchronized void removeMediaSource(int index) {
|
public final synchronized void removeMediaSource(int index) {
|
||||||
removePublicMediaSources(index, index + 1, /* handler= */ null, /* actionOnCompletion= */ null);
|
removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -275,13 +284,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
*
|
*
|
||||||
* @param index The index at which the media source will be removed. This index must be in the
|
* @param index The index at which the media source will be removed. This index must be in the
|
||||||
* range of 0 <= index < {@link #getSize()}.
|
* range of 0 <= index < {@link #getSize()}.
|
||||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||||
* source has been removed from the playlist.
|
* source has been removed from the playlist.
|
||||||
*/
|
*/
|
||||||
public final synchronized void removeMediaSource(
|
public final synchronized void removeMediaSource(
|
||||||
int index, Handler handler, Runnable actionOnCompletion) {
|
int index, Handler handler, Runnable onCompletionAction) {
|
||||||
removePublicMediaSources(index, index + 1, handler, actionOnCompletion);
|
removePublicMediaSources(index, index + 1, handler, onCompletionAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -300,7 +309,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
*/
|
*/
|
||||||
public final synchronized void removeMediaSourceRange(int fromIndex, int toIndex) {
|
public final synchronized void removeMediaSourceRange(int fromIndex, int toIndex) {
|
||||||
removePublicMediaSources(
|
removePublicMediaSources(
|
||||||
fromIndex, toIndex, /* handler= */ null, /* actionOnCompletion= */ null);
|
fromIndex, toIndex, /* handler= */ null, /* onCompletionAction= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -314,15 +323,15 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
* removed. This index must be in the range of 0 <= index <= {@link #getSize()}.
|
* removed. This index must be in the range of 0 <= index <= {@link #getSize()}.
|
||||||
* @param toIndex The final range index, pointing to the first media source that will be left
|
* @param toIndex The final range index, pointing to the first media source that will be left
|
||||||
* untouched. This index must be in the range of 0 <= index <= {@link #getSize()}.
|
* untouched. This index must be in the range of 0 <= index <= {@link #getSize()}.
|
||||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||||
* source range has been removed from the playlist.
|
* source range has been removed from the playlist.
|
||||||
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
|
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
|
||||||
* {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex}
|
* {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex}
|
||||||
*/
|
*/
|
||||||
public final synchronized void removeMediaSourceRange(
|
public final synchronized void removeMediaSourceRange(
|
||||||
int fromIndex, int toIndex, Handler handler, Runnable actionOnCompletion) {
|
int fromIndex, int toIndex, Handler handler, Runnable onCompletionAction) {
|
||||||
removePublicMediaSources(fromIndex, toIndex, handler, actionOnCompletion);
|
removePublicMediaSources(fromIndex, toIndex, handler, onCompletionAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -335,7 +344,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
*/
|
*/
|
||||||
public final synchronized void moveMediaSource(int currentIndex, int newIndex) {
|
public final synchronized void moveMediaSource(int currentIndex, int newIndex) {
|
||||||
movePublicMediaSource(
|
movePublicMediaSource(
|
||||||
currentIndex, newIndex, /* handler= */ null, /* actionOnCompletion= */ null);
|
currentIndex, newIndex, /* handler= */ null, /* onCompletionAction= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -346,13 +355,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
* in the range of 0 <= index < {@link #getSize()}.
|
* in the range of 0 <= index < {@link #getSize()}.
|
||||||
* @param newIndex The target index of the media source in the playlist. This index must be in the
|
* @param newIndex The target index of the media source in the playlist. This index must be in the
|
||||||
* range of 0 <= index < {@link #getSize()}.
|
* range of 0 <= index < {@link #getSize()}.
|
||||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||||
* source has been moved.
|
* source has been moved.
|
||||||
*/
|
*/
|
||||||
public final synchronized void moveMediaSource(
|
public final synchronized void moveMediaSource(
|
||||||
int currentIndex, int newIndex, Handler handler, Runnable actionOnCompletion) {
|
int currentIndex, int newIndex, Handler handler, Runnable onCompletionAction) {
|
||||||
movePublicMediaSource(currentIndex, newIndex, handler, actionOnCompletion);
|
movePublicMediaSource(currentIndex, newIndex, handler, onCompletionAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clears the playlist. */
|
/** Clears the playlist. */
|
||||||
@ -363,12 +372,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
/**
|
/**
|
||||||
* Clears the playlist and executes a custom action on completion.
|
* Clears the playlist and executes a custom action on completion.
|
||||||
*
|
*
|
||||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the playlist
|
* @param onCompletionAction A {@link Runnable} which is executed immediately after the playlist
|
||||||
* has been cleared.
|
* has been cleared.
|
||||||
*/
|
*/
|
||||||
public final synchronized void clear(Handler handler, Runnable actionOnCompletion) {
|
public final synchronized void clear(Handler handler, Runnable onCompletionAction) {
|
||||||
removeMediaSourceRange(0, getSize(), handler, actionOnCompletion);
|
removeMediaSourceRange(0, getSize(), handler, onCompletionAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the number of media sources in the playlist. */
|
/** Returns the number of media sources in the playlist. */
|
||||||
@ -392,20 +401,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
* @param shuffleOrder A {@link ShuffleOrder}.
|
* @param shuffleOrder A {@link ShuffleOrder}.
|
||||||
*/
|
*/
|
||||||
public final synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) {
|
public final synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) {
|
||||||
setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* actionOnCompletion= */ null);
|
setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* onCompletionAction= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a new shuffle order to use when shuffling the child media sources.
|
* Sets a new shuffle order to use when shuffling the child media sources.
|
||||||
*
|
*
|
||||||
* @param shuffleOrder A {@link ShuffleOrder}.
|
* @param shuffleOrder A {@link ShuffleOrder}.
|
||||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the shuffle
|
* @param onCompletionAction A {@link Runnable} which is executed immediately after the shuffle
|
||||||
* order has been changed.
|
* order has been changed.
|
||||||
*/
|
*/
|
||||||
public final synchronized void setShuffleOrder(
|
public final synchronized void setShuffleOrder(
|
||||||
ShuffleOrder shuffleOrder, Handler handler, Runnable actionOnCompletion) {
|
ShuffleOrder shuffleOrder, Handler handler, Runnable onCompletionAction) {
|
||||||
setPublicShuffleOrder(shuffleOrder, handler, actionOnCompletion);
|
setPublicShuffleOrder(shuffleOrder, handler, onCompletionAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompositeMediaSource implementation.
|
// CompositeMediaSource implementation.
|
||||||
@ -422,11 +431,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
super.prepareSourceInternal(mediaTransferListener);
|
super.prepareSourceInternal(mediaTransferListener);
|
||||||
playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);
|
playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);
|
||||||
if (mediaSourcesPublic.isEmpty()) {
|
if (mediaSourcesPublic.isEmpty()) {
|
||||||
notifyListener();
|
updateTimelineAndScheduleOnCompletionActions();
|
||||||
} else {
|
} else {
|
||||||
shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());
|
shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());
|
||||||
addMediaSourcesInternal(0, mediaSourcesPublic);
|
addMediaSourcesInternal(0, mediaSourcesPublic);
|
||||||
scheduleListenerNotification();
|
scheduleTimelineUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -438,7 +447,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
public final MediaPeriod createPeriod(
|
||||||
|
MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||||
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
|
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
|
||||||
MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid);
|
MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid);
|
||||||
if (holder == null) {
|
if (holder == null) {
|
||||||
@ -446,7 +456,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
holder = new MediaSourceHolder(new DummyMediaSource());
|
holder = new MediaSourceHolder(new DummyMediaSource());
|
||||||
holder.hasStartedPreparing = true;
|
holder.hasStartedPreparing = true;
|
||||||
}
|
}
|
||||||
DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, id, allocator);
|
DeferredMediaPeriod mediaPeriod =
|
||||||
|
new DeferredMediaPeriod(holder.mediaSource, id, allocator, startPositionUs);
|
||||||
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
|
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
|
||||||
holder.activeMediaPeriods.add(mediaPeriod);
|
holder.activeMediaPeriods.add(mediaPeriod);
|
||||||
if (!holder.hasStartedPreparing) {
|
if (!holder.hasStartedPreparing) {
|
||||||
@ -473,10 +484,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
super.releaseSourceInternal();
|
super.releaseSourceInternal();
|
||||||
mediaSourceHolders.clear();
|
mediaSourceHolders.clear();
|
||||||
mediaSourceByUid.clear();
|
mediaSourceByUid.clear();
|
||||||
playbackThreadHandler = null;
|
|
||||||
shuffleOrder = shuffleOrder.cloneAndClear();
|
shuffleOrder = shuffleOrder.cloneAndClear();
|
||||||
windowCount = 0;
|
windowCount = 0;
|
||||||
periodCount = 0;
|
periodCount = 0;
|
||||||
|
if (playbackThreadHandler != null) {
|
||||||
|
playbackThreadHandler.removeCallbacksAndMessages(null);
|
||||||
|
playbackThreadHandler = null;
|
||||||
|
}
|
||||||
|
timelineUpdateScheduled = false;
|
||||||
|
nextTimelineUpdateOnCompletionActions.clear();
|
||||||
|
dispatchOnCompletionActions(pendingOnCompletionActions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -516,8 +533,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
int index,
|
int index,
|
||||||
Collection<MediaSource> mediaSources,
|
Collection<MediaSource> mediaSources,
|
||||||
@Nullable Handler handler,
|
@Nullable Handler handler,
|
||||||
@Nullable Runnable actionOnCompletion) {
|
@Nullable Runnable onCompletionAction) {
|
||||||
Assertions.checkArgument((handler == null) == (actionOnCompletion == null));
|
Assertions.checkArgument((handler == null) == (onCompletionAction == null));
|
||||||
|
Handler playbackThreadHandler = this.playbackThreadHandler;
|
||||||
for (MediaSource mediaSource : mediaSources) {
|
for (MediaSource mediaSource : mediaSources) {
|
||||||
Assertions.checkNotNull(mediaSource);
|
Assertions.checkNotNull(mediaSource);
|
||||||
}
|
}
|
||||||
@ -527,12 +545,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
}
|
}
|
||||||
mediaSourcesPublic.addAll(index, mediaSourceHolders);
|
mediaSourcesPublic.addAll(index, mediaSourceHolders);
|
||||||
if (playbackThreadHandler != null && !mediaSources.isEmpty()) {
|
if (playbackThreadHandler != null && !mediaSources.isEmpty()) {
|
||||||
|
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
|
||||||
playbackThreadHandler
|
playbackThreadHandler
|
||||||
.obtainMessage(
|
.obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, callbackAction))
|
||||||
MSG_ADD, new MessageData<>(index, mediaSourceHolders, handler, actionOnCompletion))
|
|
||||||
.sendToTarget();
|
.sendToTarget();
|
||||||
} else if (actionOnCompletion != null && handler != null) {
|
} else if (onCompletionAction != null && handler != null) {
|
||||||
handler.post(actionOnCompletion);
|
handler.post(onCompletionAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -541,16 +559,17 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
int fromIndex,
|
int fromIndex,
|
||||||
int toIndex,
|
int toIndex,
|
||||||
@Nullable Handler handler,
|
@Nullable Handler handler,
|
||||||
@Nullable Runnable actionOnCompletion) {
|
@Nullable Runnable onCompletionAction) {
|
||||||
Assertions.checkArgument((handler == null) == (actionOnCompletion == null));
|
Assertions.checkArgument((handler == null) == (onCompletionAction == null));
|
||||||
|
Handler playbackThreadHandler = this.playbackThreadHandler;
|
||||||
Util.removeRange(mediaSourcesPublic, fromIndex, toIndex);
|
Util.removeRange(mediaSourcesPublic, fromIndex, toIndex);
|
||||||
if (playbackThreadHandler != null) {
|
if (playbackThreadHandler != null) {
|
||||||
|
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
|
||||||
playbackThreadHandler
|
playbackThreadHandler
|
||||||
.obtainMessage(
|
.obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, callbackAction))
|
||||||
MSG_REMOVE, new MessageData<>(fromIndex, toIndex, handler, actionOnCompletion))
|
|
||||||
.sendToTarget();
|
.sendToTarget();
|
||||||
} else if (actionOnCompletion != null && handler != null) {
|
} else if (onCompletionAction != null && handler != null) {
|
||||||
handler.post(actionOnCompletion);
|
handler.post(onCompletionAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,23 +578,24 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
int currentIndex,
|
int currentIndex,
|
||||||
int newIndex,
|
int newIndex,
|
||||||
@Nullable Handler handler,
|
@Nullable Handler handler,
|
||||||
@Nullable Runnable actionOnCompletion) {
|
@Nullable Runnable onCompletionAction) {
|
||||||
Assertions.checkArgument((handler == null) == (actionOnCompletion == null));
|
Assertions.checkArgument((handler == null) == (onCompletionAction == null));
|
||||||
|
Handler playbackThreadHandler = this.playbackThreadHandler;
|
||||||
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
|
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
|
||||||
if (playbackThreadHandler != null) {
|
if (playbackThreadHandler != null) {
|
||||||
|
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
|
||||||
playbackThreadHandler
|
playbackThreadHandler
|
||||||
.obtainMessage(
|
.obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, callbackAction))
|
||||||
MSG_MOVE, new MessageData<>(currentIndex, newIndex, handler, actionOnCompletion))
|
|
||||||
.sendToTarget();
|
.sendToTarget();
|
||||||
} else if (actionOnCompletion != null && handler != null) {
|
} else if (onCompletionAction != null && handler != null) {
|
||||||
handler.post(actionOnCompletion);
|
handler.post(onCompletionAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
private void setPublicShuffleOrder(
|
private void setPublicShuffleOrder(
|
||||||
ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable actionOnCompletion) {
|
ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable onCompletionAction) {
|
||||||
Assertions.checkArgument((handler == null) == (actionOnCompletion == null));
|
Assertions.checkArgument((handler == null) == (onCompletionAction == null));
|
||||||
Handler playbackThreadHandler = this.playbackThreadHandler;
|
Handler playbackThreadHandler = this.playbackThreadHandler;
|
||||||
if (playbackThreadHandler != null) {
|
if (playbackThreadHandler != null) {
|
||||||
int size = getSize();
|
int size = getSize();
|
||||||
@ -585,35 +605,44 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
.cloneAndClear()
|
.cloneAndClear()
|
||||||
.cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
|
.cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
|
||||||
}
|
}
|
||||||
|
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
|
||||||
playbackThreadHandler
|
playbackThreadHandler
|
||||||
.obtainMessage(
|
.obtainMessage(
|
||||||
MSG_SET_SHUFFLE_ORDER,
|
MSG_SET_SHUFFLE_ORDER,
|
||||||
new MessageData<>(/* index= */ 0, shuffleOrder, handler, actionOnCompletion))
|
new MessageData<>(/* index= */ 0, shuffleOrder, callbackAction))
|
||||||
.sendToTarget();
|
.sendToTarget();
|
||||||
} else {
|
} else {
|
||||||
this.shuffleOrder =
|
this.shuffleOrder =
|
||||||
shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
|
shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
|
||||||
if (actionOnCompletion != null && handler != null) {
|
if (onCompletionAction != null && handler != null) {
|
||||||
handler.post(actionOnCompletion);
|
handler.post(onCompletionAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
@Nullable
|
||||||
|
private HandlerAndRunnable createOnCompletionAction(
|
||||||
|
@Nullable Handler handler, @Nullable Runnable runnable) {
|
||||||
|
if (handler == null || runnable == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
HandlerAndRunnable handlerAndRunnable = new HandlerAndRunnable(handler, runnable);
|
||||||
|
pendingOnCompletionActions.add(handlerAndRunnable);
|
||||||
|
return handlerAndRunnable;
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods. Called on the playback thread.
|
// Internal methods. Called on the playback thread.
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private boolean handleMessage(Message msg) {
|
private boolean handleMessage(Message msg) {
|
||||||
if (playbackThreadHandler == null) {
|
|
||||||
// Stale event.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
switch (msg.what) {
|
switch (msg.what) {
|
||||||
case MSG_ADD:
|
case MSG_ADD:
|
||||||
MessageData<Collection<MediaSourceHolder>> addMessage =
|
MessageData<Collection<MediaSourceHolder>> addMessage =
|
||||||
(MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj);
|
(MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj);
|
||||||
shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size());
|
shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size());
|
||||||
addMediaSourcesInternal(addMessage.index, addMessage.customData);
|
addMediaSourcesInternal(addMessage.index, addMessage.customData);
|
||||||
scheduleListenerNotification(addMessage.handler, addMessage.actionOnCompletion);
|
scheduleTimelineUpdate(addMessage.onCompletionAction);
|
||||||
break;
|
break;
|
||||||
case MSG_REMOVE:
|
case MSG_REMOVE:
|
||||||
MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
|
MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
|
||||||
@ -627,29 +656,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
for (int index = toIndex - 1; index >= fromIndex; index--) {
|
for (int index = toIndex - 1; index >= fromIndex; index--) {
|
||||||
removeMediaSourceInternal(index);
|
removeMediaSourceInternal(index);
|
||||||
}
|
}
|
||||||
scheduleListenerNotification(removeMessage.handler, removeMessage.actionOnCompletion);
|
scheduleTimelineUpdate(removeMessage.onCompletionAction);
|
||||||
break;
|
break;
|
||||||
case MSG_MOVE:
|
case MSG_MOVE:
|
||||||
MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
|
MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
|
||||||
shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1);
|
shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1);
|
||||||
shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);
|
shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);
|
||||||
moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
|
moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
|
||||||
scheduleListenerNotification(moveMessage.handler, moveMessage.actionOnCompletion);
|
scheduleTimelineUpdate(moveMessage.onCompletionAction);
|
||||||
break;
|
break;
|
||||||
case MSG_SET_SHUFFLE_ORDER:
|
case MSG_SET_SHUFFLE_ORDER:
|
||||||
MessageData<ShuffleOrder> shuffleOrderMessage =
|
MessageData<ShuffleOrder> shuffleOrderMessage =
|
||||||
(MessageData<ShuffleOrder>) Util.castNonNull(msg.obj);
|
(MessageData<ShuffleOrder>) Util.castNonNull(msg.obj);
|
||||||
shuffleOrder = shuffleOrderMessage.customData;
|
shuffleOrder = shuffleOrderMessage.customData;
|
||||||
scheduleListenerNotification(
|
scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction);
|
||||||
shuffleOrderMessage.handler, shuffleOrderMessage.actionOnCompletion);
|
|
||||||
break;
|
break;
|
||||||
case MSG_NOTIFY_LISTENER:
|
case MSG_UPDATE_TIMELINE:
|
||||||
notifyListener();
|
updateTimelineAndScheduleOnCompletionActions();
|
||||||
break;
|
break;
|
||||||
case MSG_ON_COMPLETION:
|
case MSG_ON_COMPLETION:
|
||||||
EventDispatcher<Runnable> actionsOnCompletion =
|
Set<HandlerAndRunnable> actions = (Set<HandlerAndRunnable>) Util.castNonNull(msg.obj);
|
||||||
(EventDispatcher<Runnable>) Util.castNonNull(msg.obj);
|
dispatchOnCompletionActions(actions);
|
||||||
actionsOnCompletion.dispatch(Runnable::run);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
@ -657,36 +684,48 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleListenerNotification() {
|
private void scheduleTimelineUpdate() {
|
||||||
scheduleListenerNotification(/* handler= */ null, /* actionOnCompletion= */ null);
|
scheduleTimelineUpdate(/* onCompletionAction= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleListenerNotification(
|
private void scheduleTimelineUpdate(@Nullable HandlerAndRunnable onCompletionAction) {
|
||||||
@Nullable Handler handler, @Nullable Runnable actionOnCompletion) {
|
if (!timelineUpdateScheduled) {
|
||||||
if (!listenerNotificationScheduled) {
|
getPlaybackThreadHandlerOnPlaybackThread().obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget();
|
||||||
Assertions.checkNotNull(playbackThreadHandler)
|
timelineUpdateScheduled = true;
|
||||||
.obtainMessage(MSG_NOTIFY_LISTENER)
|
|
||||||
.sendToTarget();
|
|
||||||
listenerNotificationScheduled = true;
|
|
||||||
}
|
}
|
||||||
if (actionOnCompletion != null && handler != null) {
|
if (onCompletionAction != null) {
|
||||||
pendingOnCompletionActions.addListener(handler, actionOnCompletion);
|
nextTimelineUpdateOnCompletionActions.add(onCompletionAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyListener() {
|
private void updateTimelineAndScheduleOnCompletionActions() {
|
||||||
listenerNotificationScheduled = false;
|
timelineUpdateScheduled = false;
|
||||||
EventDispatcher<Runnable> actionsOnCompletion = pendingOnCompletionActions;
|
Set<HandlerAndRunnable> onCompletionActions = nextTimelineUpdateOnCompletionActions;
|
||||||
pendingOnCompletionActions = new EventDispatcher<>();
|
nextTimelineUpdateOnCompletionActions = new HashSet<>();
|
||||||
refreshSourceInfo(
|
refreshSourceInfo(
|
||||||
new ConcatenatedTimeline(
|
new ConcatenatedTimeline(
|
||||||
mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
|
mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
|
||||||
/* manifest= */ null);
|
/* manifest= */ null);
|
||||||
Assertions.checkNotNull(playbackThreadHandler)
|
getPlaybackThreadHandlerOnPlaybackThread()
|
||||||
.obtainMessage(MSG_ON_COMPLETION, actionsOnCompletion)
|
.obtainMessage(MSG_ON_COMPLETION, onCompletionActions)
|
||||||
.sendToTarget();
|
.sendToTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("GuardedBy")
|
||||||
|
private Handler getPlaybackThreadHandlerOnPlaybackThread() {
|
||||||
|
// Write access to this value happens on the playback thread only, so playback thread reads
|
||||||
|
// don't need to be synchronized.
|
||||||
|
return Assertions.checkNotNull(playbackThreadHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void dispatchOnCompletionActions(
|
||||||
|
Set<HandlerAndRunnable> onCompletionActions) {
|
||||||
|
for (HandlerAndRunnable pendingAction : onCompletionActions) {
|
||||||
|
pendingAction.dispatch();
|
||||||
|
}
|
||||||
|
pendingOnCompletionActions.removeAll(onCompletionActions);
|
||||||
|
}
|
||||||
|
|
||||||
private void addMediaSourcesInternal(
|
private void addMediaSourcesInternal(
|
||||||
int index, Collection<MediaSourceHolder> mediaSourceHolders) {
|
int index, Collection<MediaSourceHolder> mediaSourceHolders) {
|
||||||
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
|
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
|
||||||
@ -761,6 +800,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
// unlikely to be a problem as a non-zero default position usually only occurs for live
|
// unlikely to be a problem as a non-zero default position usually only occurs for live
|
||||||
// playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions
|
// playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions
|
||||||
// anyway.
|
// anyway.
|
||||||
|
timeline.getWindow(/* windowIndex= */ 0, window);
|
||||||
long windowStartPositionUs = window.getDefaultPositionUs();
|
long windowStartPositionUs = window.getDefaultPositionUs();
|
||||||
if (deferredMediaPeriod != null) {
|
if (deferredMediaPeriod != null) {
|
||||||
long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs();
|
long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs();
|
||||||
@ -782,7 +822,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
mediaSourceHolder.isPrepared = true;
|
mediaSourceHolder.isPrepared = true;
|
||||||
scheduleListenerNotification();
|
scheduleTimelineUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeMediaSourceInternal(int index) {
|
private void removeMediaSourceInternal(int index) {
|
||||||
@ -895,15 +935,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
|
|
||||||
public final int index;
|
public final int index;
|
||||||
public final T customData;
|
public final T customData;
|
||||||
@Nullable public final Handler handler;
|
@Nullable public final HandlerAndRunnable onCompletionAction;
|
||||||
@Nullable public final Runnable actionOnCompletion;
|
|
||||||
|
|
||||||
public MessageData(
|
public MessageData(int index, T customData, @Nullable HandlerAndRunnable onCompletionAction) {
|
||||||
int index, T customData, @Nullable Handler handler, @Nullable Runnable actionOnCompletion) {
|
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.customData = customData;
|
this.customData = customData;
|
||||||
this.handler = handler;
|
this.onCompletionAction = onCompletionAction;
|
||||||
this.actionOnCompletion = actionOnCompletion;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1144,7 +1181,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1153,5 +1190,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class HandlerAndRunnable {
|
||||||
|
|
||||||
|
private final Handler handler;
|
||||||
|
private final Runnable runnable;
|
||||||
|
|
||||||
|
public HandlerAndRunnable(Handler handler, Runnable runnable) {
|
||||||
|
this.handler = handler;
|
||||||
|
this.runnable = runnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispatch() {
|
||||||
|
handler.post(runnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import java.io.IOException;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Media period that wraps a media source and defers calling its {@link
|
* Media period that wraps a media source and defers calling its {@link
|
||||||
* MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link
|
* MediaSource#createPeriod(MediaPeriodId, Allocator, long)} method until {@link
|
||||||
* #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media
|
* #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media
|
||||||
* period immediately but the media source that should create it is not yet prepared.
|
* period immediately but the media source that should create it is not yet prepared.
|
||||||
*/
|
*/
|
||||||
@ -60,11 +60,14 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
|||||||
* @param mediaSource The media source to wrap.
|
* @param mediaSource The media source to wrap.
|
||||||
* @param id The identifier used to create the deferred media period.
|
* @param id The identifier used to create the deferred media period.
|
||||||
* @param allocator The allocator used to create the media period.
|
* @param allocator The allocator used to create the media period.
|
||||||
|
* @param preparePositionUs The expected start position, in microseconds.
|
||||||
*/
|
*/
|
||||||
public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
|
public DeferredMediaPeriod(
|
||||||
|
MediaSource mediaSource, MediaPeriodId id, Allocator allocator, long preparePositionUs) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.allocator = allocator;
|
this.allocator = allocator;
|
||||||
this.mediaSource = mediaSource;
|
this.mediaSource = mediaSource;
|
||||||
|
this.preparePositionUs = preparePositionUs;
|
||||||
preparePositionOverrideUs = C.TIME_UNSET;
|
preparePositionOverrideUs = C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,28 +89,25 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides the default prepare position at which to prepare the media period. This value is only
|
* Overrides the default prepare position at which to prepare the media period. This value is only
|
||||||
* used if the call to {@link MediaPeriod#prepare(Callback, long)} is being deferred.
|
* used if called before {@link #createPeriod(MediaPeriodId)}.
|
||||||
*
|
*
|
||||||
* @param defaultPreparePositionUs The default prepare position to use, in microseconds.
|
* @param preparePositionUs The default prepare position to use, in microseconds.
|
||||||
*/
|
*/
|
||||||
public void overridePreparePositionUs(long defaultPreparePositionUs) {
|
public void overridePreparePositionUs(long preparePositionUs) {
|
||||||
preparePositionOverrideUs = defaultPreparePositionUs;
|
preparePositionOverrideUs = preparePositionUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then
|
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source
|
||||||
* prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()}
|
* then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link
|
||||||
* to release the period.
|
* #releasePeriod()} to release the period.
|
||||||
*
|
*
|
||||||
* @param id The identifier that should be used to create the media period from the media source.
|
* @param id The identifier that should be used to create the media period from the media source.
|
||||||
*/
|
*/
|
||||||
public void createPeriod(MediaPeriodId id) {
|
public void createPeriod(MediaPeriodId id) {
|
||||||
mediaPeriod = mediaSource.createPeriod(id, allocator);
|
long preparePositionUs = getPreparePositionWithOverride(this.preparePositionUs);
|
||||||
|
mediaPeriod = mediaSource.createPeriod(id, allocator, preparePositionUs);
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
long preparePositionUs =
|
|
||||||
preparePositionOverrideUs != C.TIME_UNSET
|
|
||||||
? preparePositionOverrideUs
|
|
||||||
: this.preparePositionUs;
|
|
||||||
mediaPeriod.prepare(this, preparePositionUs);
|
mediaPeriod.prepare(this, preparePositionUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,9 +124,8 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
|||||||
@Override
|
@Override
|
||||||
public void prepare(Callback callback, long preparePositionUs) {
|
public void prepare(Callback callback, long preparePositionUs) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.preparePositionUs = preparePositionUs;
|
|
||||||
if (mediaPeriod != null) {
|
if (mediaPeriod != null) {
|
||||||
mediaPeriod.prepare(this, preparePositionUs);
|
mediaPeriod.prepare(this, getPreparePositionWithOverride(this.preparePositionUs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,4 +216,9 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
|||||||
callback.onPrepared(this);
|
callback.onPrepared(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long getPreparePositionWithOverride(long preparePositionUs) {
|
||||||
|
return preparePositionOverrideUs != C.TIME_UNSET
|
||||||
|
? preparePositionOverrideUs
|
||||||
|
: preparePositionUs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import android.os.Handler;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
@ -32,25 +33,13 @@ import com.google.android.exoplayer2.upstream.TransferListener;
|
|||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/** @deprecated Use {@link ProgressiveMediaSource} instead. */
|
||||||
* Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}.
|
@Deprecated
|
||||||
*
|
@SuppressWarnings("deprecation")
|
||||||
* <p>If the possible input stream container formats are known, pass a factory that instantiates
|
|
||||||
* extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to use
|
|
||||||
* the default extractors. When reading a new stream, the first {@link Extractor} in the array of
|
|
||||||
* extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be
|
|
||||||
* used to extract samples from the input stream.
|
|
||||||
*
|
|
||||||
* <p>Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking.
|
|
||||||
*/
|
|
||||||
public final class ExtractorMediaSource extends BaseMediaSource
|
public final class ExtractorMediaSource extends BaseMediaSource
|
||||||
implements ExtractorMediaPeriod.Listener {
|
implements MediaSource.SourceInfoRefreshListener {
|
||||||
|
|
||||||
/**
|
/** @deprecated Use {@link MediaSourceEventListener} instead. */
|
||||||
* Listener of {@link ExtractorMediaSource} events.
|
|
||||||
*
|
|
||||||
* @deprecated Use {@link MediaSourceEventListener}.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public interface EventListener {
|
public interface EventListener {
|
||||||
|
|
||||||
@ -70,7 +59,8 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Factory for {@link ExtractorMediaSource}s. */
|
/** Use {@link ProgressiveMediaSource.Factory} instead. */
|
||||||
|
@Deprecated
|
||||||
public static final class Factory implements AdsMediaSource.MediaSourceFactory {
|
public static final class Factory implements AdsMediaSource.MediaSourceFactory {
|
||||||
|
|
||||||
private final DataSource.Factory dataSourceFactory;
|
private final DataSource.Factory dataSourceFactory;
|
||||||
@ -232,23 +222,11 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Deprecated
|
||||||
* The default number of bytes that should be loaded between each each invocation of {@link
|
public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES =
|
||||||
* MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
|
ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
|
||||||
*/
|
|
||||||
public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;
|
|
||||||
|
|
||||||
private final Uri uri;
|
private final ProgressiveMediaSource progressiveMediaSource;
|
||||||
private final DataSource.Factory dataSourceFactory;
|
|
||||||
private final ExtractorsFactory extractorsFactory;
|
|
||||||
private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;
|
|
||||||
private final String customCacheKey;
|
|
||||||
private final int continueLoadingCheckIntervalBytes;
|
|
||||||
private final @Nullable Object tag;
|
|
||||||
|
|
||||||
private long timelineDurationUs;
|
|
||||||
private boolean timelineIsSeekable;
|
|
||||||
private @Nullable TransferListener transferListener;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param uri The {@link Uri} of the media stream.
|
* @param uri The {@link Uri} of the media stream.
|
||||||
@ -261,7 +239,6 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
|||||||
* @deprecated Use {@link Factory} instead.
|
* @deprecated Use {@link Factory} instead.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public ExtractorMediaSource(
|
public ExtractorMediaSource(
|
||||||
Uri uri,
|
Uri uri,
|
||||||
DataSource.Factory dataSourceFactory,
|
DataSource.Factory dataSourceFactory,
|
||||||
@ -284,7 +261,6 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
|||||||
* @deprecated Use {@link Factory} instead.
|
* @deprecated Use {@link Factory} instead.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public ExtractorMediaSource(
|
public ExtractorMediaSource(
|
||||||
Uri uri,
|
Uri uri,
|
||||||
DataSource.Factory dataSourceFactory,
|
DataSource.Factory dataSourceFactory,
|
||||||
@ -317,7 +293,6 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
|||||||
* @deprecated Use {@link Factory} instead.
|
* @deprecated Use {@link Factory} instead.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public ExtractorMediaSource(
|
public ExtractorMediaSource(
|
||||||
Uri uri,
|
Uri uri,
|
||||||
DataSource.Factory dataSourceFactory,
|
DataSource.Factory dataSourceFactory,
|
||||||
@ -347,93 +322,57 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
|||||||
@Nullable String customCacheKey,
|
@Nullable String customCacheKey,
|
||||||
int continueLoadingCheckIntervalBytes,
|
int continueLoadingCheckIntervalBytes,
|
||||||
@Nullable Object tag) {
|
@Nullable Object tag) {
|
||||||
this.uri = uri;
|
progressiveMediaSource =
|
||||||
this.dataSourceFactory = dataSourceFactory;
|
new ProgressiveMediaSource(
|
||||||
this.extractorsFactory = extractorsFactory;
|
uri,
|
||||||
this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy;
|
dataSourceFactory,
|
||||||
this.customCacheKey = customCacheKey;
|
extractorsFactory,
|
||||||
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
loadableLoadErrorHandlingPolicy,
|
||||||
this.timelineDurationUs = C.TIME_UNSET;
|
customCacheKey,
|
||||||
this.tag = tag;
|
continueLoadingCheckIntervalBytes,
|
||||||
|
tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public Object getTag() {
|
public Object getTag() {
|
||||||
return tag;
|
return progressiveMediaSource.getTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
transferListener = mediaTransferListener;
|
progressiveMediaSource.prepareSource(/* listener= */ this, mediaTransferListener);
|
||||||
notifySourceInfoRefreshed(timelineDurationUs, /* isSeekable= */ false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||||
// Do nothing.
|
progressiveMediaSource.maybeThrowSourceInfoRefreshError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||||
DataSource dataSource = dataSourceFactory.createDataSource();
|
return progressiveMediaSource.createPeriod(id, allocator, startPositionUs);
|
||||||
if (transferListener != null) {
|
|
||||||
dataSource.addTransferListener(transferListener);
|
|
||||||
}
|
|
||||||
return new ExtractorMediaPeriod(
|
|
||||||
uri,
|
|
||||||
dataSource,
|
|
||||||
extractorsFactory.createExtractors(),
|
|
||||||
loadableLoadErrorHandlingPolicy,
|
|
||||||
createEventDispatcher(id),
|
|
||||||
this,
|
|
||||||
allocator,
|
|
||||||
customCacheKey,
|
|
||||||
continueLoadingCheckIntervalBytes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||||
((ExtractorMediaPeriod) mediaPeriod).release();
|
progressiveMediaSource.releasePeriod(mediaPeriod);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void releaseSourceInternal() {
|
public void releaseSourceInternal() {
|
||||||
// Do nothing.
|
progressiveMediaSource.releaseSource(/* listener= */ this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractorMediaPeriod.Listener implementation.
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) {
|
public void onSourceInfoRefreshed(
|
||||||
// If we already have the duration from a previous source info refresh, use it.
|
MediaSource source, Timeline timeline, @Nullable Object manifest) {
|
||||||
durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs;
|
refreshSourceInfo(timeline, manifest);
|
||||||
if (timelineDurationUs == durationUs && timelineIsSeekable == isSeekable) {
|
|
||||||
// Suppress no-op source info changes.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
notifySourceInfoRefreshed(durationUs, isSeekable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal methods.
|
|
||||||
|
|
||||||
private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) {
|
|
||||||
timelineDurationUs = durationUs;
|
|
||||||
timelineIsSeekable = isSeekable;
|
|
||||||
// TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223.
|
|
||||||
refreshSourceInfo(
|
|
||||||
new SinglePeriodTimeline(
|
|
||||||
timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, tag),
|
|
||||||
/* manifest= */ null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in
|
|
||||||
* {@link MediaSourceEventListener}.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
private static final class EventListenerWrapper extends DefaultMediaSourceEventListener {
|
private static final class EventListenerWrapper extends DefaultMediaSourceEventListener {
|
||||||
|
|
||||||
private final EventListener eventListener;
|
private final EventListener eventListener;
|
||||||
|
|
||||||
public EventListenerWrapper(EventListener eventListener) {
|
public EventListenerWrapper(EventListener eventListener) {
|
||||||
|
@ -31,7 +31,7 @@ import java.util.Map;
|
|||||||
* Loops a {@link MediaSource} a specified number of times.
|
* Loops a {@link MediaSource} a specified number of times.
|
||||||
*
|
*
|
||||||
* <p>Note: To loop a {@link MediaSource} indefinitely, it is usually better to use {@link
|
* <p>Note: To loop a {@link MediaSource} indefinitely, it is usually better to use {@link
|
||||||
* ExoPlayer#setRepeatMode(int)}.
|
* ExoPlayer#setRepeatMode(int)} instead of this class.
|
||||||
*/
|
*/
|
||||||
public final class LoopingMediaSource extends CompositeMediaSource<Void> {
|
public final class LoopingMediaSource extends CompositeMediaSource<Void> {
|
||||||
|
|
||||||
@ -77,14 +77,15 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||||
if (loopCount == Integer.MAX_VALUE) {
|
if (loopCount == Integer.MAX_VALUE) {
|
||||||
return childSource.createPeriod(id, allocator);
|
return childSource.createPeriod(id, allocator, startPositionUs);
|
||||||
}
|
}
|
||||||
Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid);
|
Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid);
|
||||||
MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid);
|
MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid);
|
||||||
childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id);
|
childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id);
|
||||||
MediaPeriod mediaPeriod = childSource.createPeriod(childMediaPeriodId, allocator);
|
MediaPeriod mediaPeriod =
|
||||||
|
childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
|
||||||
mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId);
|
mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId);
|
||||||
return mediaPeriod;
|
return mediaPeriod;
|
||||||
}
|
}
|
||||||
|
@ -87,18 +87,18 @@ public interface MediaPeriod extends SequenceableLoader {
|
|||||||
TrackGroupArray getTrackGroups();
|
TrackGroupArray getTrackGroups();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of {@link StreamKey stream keys} which allow to filter the media in this period
|
* Returns a list of {@link StreamKey StreamKeys} which allow to filter the media in this period
|
||||||
* to load only the parts needed to play the provided {@link TrackSelection}.
|
* to load only the parts needed to play the provided {@link TrackSelection TrackSelections}.
|
||||||
*
|
*
|
||||||
* <p>This method is only called after the period has been prepared.
|
* <p>This method is only called after the period has been prepared.
|
||||||
*
|
*
|
||||||
* @param trackSelection The {@link TrackSelection} describing the tracks for which stream keys
|
* @param trackSelections The {@link TrackSelection TrackSelections} describing the tracks for
|
||||||
* are requested.
|
* which stream keys are requested.
|
||||||
* @return The corresponding {@link StreamKey stream keys} for the selected tracks, or an empty
|
* @return The corresponding {@link StreamKey StreamKeys} for the selected tracks, or an empty
|
||||||
* list if filtering is not possible and the entire media needs to be loaded to play the
|
* list if filtering is not possible and the entire media needs to be loaded to play the
|
||||||
* selected tracks.
|
* selected tracks.
|
||||||
*/
|
*/
|
||||||
default List<StreamKey> getStreamKeys(TrackSelection trackSelection) {
|
default List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.upstream.Allocator;
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
@ -34,8 +35,8 @@ import java.io.IOException;
|
|||||||
* on the {@link SourceInfoRefreshListener}s passed to {@link
|
* on the {@link SourceInfoRefreshListener}s passed to {@link
|
||||||
* #prepareSource(SourceInfoRefreshListener, TransferListener)}.
|
* #prepareSource(SourceInfoRefreshListener, TransferListener)}.
|
||||||
* <li>To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are
|
* <li>To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are
|
||||||
* obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for
|
* obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a
|
||||||
* the player to load and read the media.
|
* way for the player to load and read the media.
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* All methods are called on the player's internal playback thread, as described in the {@link
|
* All methods are called on the player's internal playback thread, as described in the {@link
|
||||||
@ -89,12 +90,10 @@ public interface MediaSource {
|
|||||||
public final long windowSequenceNumber;
|
public final long windowSequenceNumber;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The end position to which the media period's content is clipped in order to play a following
|
* The index of the next ad group to which the media period's content is clipped, or {@link
|
||||||
* ad group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if
|
* C#INDEX_UNSET} if there is no following ad group or if this media period is an ad.
|
||||||
* this media period is an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll
|
|
||||||
* ad follows at the end of this content media period.
|
|
||||||
*/
|
*/
|
||||||
public final long endPositionUs;
|
public final int nextAdGroupIndex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a media period identifier for a dummy period which is not part of a buffered sequence
|
* Creates a media period identifier for a dummy period which is not part of a buffered sequence
|
||||||
@ -103,7 +102,7 @@ public interface MediaSource {
|
|||||||
* @param periodUid The unique id of the timeline period.
|
* @param periodUid The unique id of the timeline period.
|
||||||
*/
|
*/
|
||||||
public MediaPeriodId(Object periodUid) {
|
public MediaPeriodId(Object periodUid) {
|
||||||
this(periodUid, C.INDEX_UNSET);
|
this(periodUid, /* windowSequenceNumber= */ C.INDEX_UNSET);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,7 +113,12 @@ public interface MediaSource {
|
|||||||
* windows this media period is part of.
|
* windows this media period is part of.
|
||||||
*/
|
*/
|
||||||
public MediaPeriodId(Object periodUid, long windowSequenceNumber) {
|
public MediaPeriodId(Object periodUid, long windowSequenceNumber) {
|
||||||
this(periodUid, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, C.TIME_UNSET);
|
this(
|
||||||
|
periodUid,
|
||||||
|
/* adGroupIndex= */ C.INDEX_UNSET,
|
||||||
|
/* adIndexInAdGroup= */ C.INDEX_UNSET,
|
||||||
|
windowSequenceNumber,
|
||||||
|
/* nextAdGroupIndex= */ C.INDEX_UNSET);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,11 +127,16 @@ public interface MediaSource {
|
|||||||
* @param periodUid The unique id of the timeline period.
|
* @param periodUid The unique id of the timeline period.
|
||||||
* @param windowSequenceNumber The sequence number of the window in the buffered sequence of
|
* @param windowSequenceNumber The sequence number of the window in the buffered sequence of
|
||||||
* windows this media period is part of.
|
* windows this media period is part of.
|
||||||
* @param endPositionUs The end position of the media period within the timeline period, in
|
* @param nextAdGroupIndex The index of the next ad group to which the media period's content is
|
||||||
* microseconds.
|
* clipped.
|
||||||
*/
|
*/
|
||||||
public MediaPeriodId(Object periodUid, long windowSequenceNumber, long endPositionUs) {
|
public MediaPeriodId(Object periodUid, long windowSequenceNumber, int nextAdGroupIndex) {
|
||||||
this(periodUid, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, endPositionUs);
|
this(
|
||||||
|
periodUid,
|
||||||
|
/* adGroupIndex= */ C.INDEX_UNSET,
|
||||||
|
/* adIndexInAdGroup= */ C.INDEX_UNSET,
|
||||||
|
windowSequenceNumber,
|
||||||
|
nextAdGroupIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -142,7 +151,12 @@ public interface MediaSource {
|
|||||||
*/
|
*/
|
||||||
public MediaPeriodId(
|
public MediaPeriodId(
|
||||||
Object periodUid, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) {
|
Object periodUid, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) {
|
||||||
this(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, C.TIME_UNSET);
|
this(
|
||||||
|
periodUid,
|
||||||
|
adGroupIndex,
|
||||||
|
adIndexInAdGroup,
|
||||||
|
windowSequenceNumber,
|
||||||
|
/* nextAdGroupIndex= */ C.INDEX_UNSET);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaPeriodId(
|
private MediaPeriodId(
|
||||||
@ -150,12 +164,12 @@ public interface MediaSource {
|
|||||||
int adGroupIndex,
|
int adGroupIndex,
|
||||||
int adIndexInAdGroup,
|
int adIndexInAdGroup,
|
||||||
long windowSequenceNumber,
|
long windowSequenceNumber,
|
||||||
long endPositionUs) {
|
int nextAdGroupIndex) {
|
||||||
this.periodUid = periodUid;
|
this.periodUid = periodUid;
|
||||||
this.adGroupIndex = adGroupIndex;
|
this.adGroupIndex = adGroupIndex;
|
||||||
this.adIndexInAdGroup = adIndexInAdGroup;
|
this.adIndexInAdGroup = adIndexInAdGroup;
|
||||||
this.windowSequenceNumber = windowSequenceNumber;
|
this.windowSequenceNumber = windowSequenceNumber;
|
||||||
this.endPositionUs = endPositionUs;
|
this.nextAdGroupIndex = nextAdGroupIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a copy of this period identifier but with {@code newPeriodUid} as its period uid. */
|
/** Returns a copy of this period identifier but with {@code newPeriodUid} as its period uid. */
|
||||||
@ -163,7 +177,7 @@ public interface MediaSource {
|
|||||||
return periodUid.equals(newPeriodUid)
|
return periodUid.equals(newPeriodUid)
|
||||||
? this
|
? this
|
||||||
: new MediaPeriodId(
|
: new MediaPeriodId(
|
||||||
newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, endPositionUs);
|
newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, nextAdGroupIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -187,7 +201,7 @@ public interface MediaSource {
|
|||||||
&& adGroupIndex == periodId.adGroupIndex
|
&& adGroupIndex == periodId.adGroupIndex
|
||||||
&& adIndexInAdGroup == periodId.adIndexInAdGroup
|
&& adIndexInAdGroup == periodId.adIndexInAdGroup
|
||||||
&& windowSequenceNumber == periodId.windowSequenceNumber
|
&& windowSequenceNumber == periodId.windowSequenceNumber
|
||||||
&& endPositionUs == periodId.endPositionUs;
|
&& nextAdGroupIndex == periodId.nextAdGroupIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -197,7 +211,7 @@ public interface MediaSource {
|
|||||||
result = 31 * result + adGroupIndex;
|
result = 31 * result + adGroupIndex;
|
||||||
result = 31 * result + adIndexInAdGroup;
|
result = 31 * result + adIndexInAdGroup;
|
||||||
result = 31 * result + (int) windowSequenceNumber;
|
result = 31 * result + (int) windowSequenceNumber;
|
||||||
result = 31 * result + (int) endPositionUs;
|
result = 31 * result + nextAdGroupIndex;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,7 +238,6 @@ public interface MediaSource {
|
|||||||
default Object getTag() {
|
default Object getTag() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts source preparation if not yet started, and adds a listener for timeline and/or manifest
|
* Starts source preparation if not yet started, and adds a listener for timeline and/or manifest
|
||||||
* updates.
|
* updates.
|
||||||
@ -243,8 +256,7 @@ public interface MediaSource {
|
|||||||
* and other data.
|
* and other data.
|
||||||
*/
|
*/
|
||||||
void prepareSource(
|
void prepareSource(
|
||||||
SourceInfoRefreshListener listener,
|
SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener);
|
||||||
@Nullable TransferListener mediaTransferListener);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Throws any pending error encountered while loading or refreshing source information.
|
* Throws any pending error encountered while loading or refreshing source information.
|
||||||
@ -261,9 +273,10 @@ public interface MediaSource {
|
|||||||
*
|
*
|
||||||
* @param id The identifier of the period.
|
* @param id The identifier of the period.
|
||||||
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
|
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
|
||||||
|
* @param startPositionUs The expected start position, in microseconds.
|
||||||
* @return A new {@link MediaPeriod}.
|
* @return A new {@link MediaPeriod}.
|
||||||
*/
|
*/
|
||||||
MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator);
|
MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Releases the period.
|
* Releases the period.
|
||||||
|
@ -120,13 +120,13 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||||
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
|
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
|
||||||
int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
|
int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
|
||||||
for (int i = 0; i < periods.length; i++) {
|
for (int i = 0; i < periods.length; i++) {
|
||||||
MediaPeriodId childMediaPeriodId =
|
MediaPeriodId childMediaPeriodId =
|
||||||
id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
|
id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
|
||||||
periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator);
|
periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs);
|
||||||
}
|
}
|
||||||
return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods);
|
return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods);
|
||||||
}
|
}
|
||||||
|
@ -54,11 +54,12 @@ import java.io.IOException;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
|
|
||||||
/**
|
/** A {@link MediaPeriod} that extracts data using an {@link Extractor}. */
|
||||||
* A {@link MediaPeriod} that extracts data using an {@link Extractor}.
|
/* package */ final class ProgressiveMediaPeriod
|
||||||
*/
|
implements MediaPeriod,
|
||||||
/* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput,
|
ExtractorOutput,
|
||||||
Loader.Callback<ExtractorMediaPeriod.ExtractingLoadable>, Loader.ReleaseCallback,
|
Loader.Callback<ProgressiveMediaPeriod.ExtractingLoadable>,
|
||||||
|
Loader.ReleaseCallback,
|
||||||
UpstreamFormatChangedListener {
|
UpstreamFormatChangedListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -145,7 +146,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
"nullness:argument.type.incompatible",
|
"nullness:argument.type.incompatible",
|
||||||
"nullness:methodref.receiver.bound.invalid"
|
"nullness:methodref.receiver.bound.invalid"
|
||||||
})
|
})
|
||||||
public ExtractorMediaPeriod(
|
public ProgressiveMediaPeriod(
|
||||||
Uri uri,
|
Uri uri,
|
||||||
DataSource dataSource,
|
DataSource dataSource,
|
||||||
Extractor[] extractors,
|
Extractor[] extractors,
|
||||||
@ -163,14 +164,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
this.allocator = allocator;
|
this.allocator = allocator;
|
||||||
this.customCacheKey = customCacheKey;
|
this.customCacheKey = customCacheKey;
|
||||||
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
||||||
loader = new Loader("Loader:ExtractorMediaPeriod");
|
loader = new Loader("Loader:ProgressiveMediaPeriod");
|
||||||
extractorHolder = new ExtractorHolder(extractors);
|
extractorHolder = new ExtractorHolder(extractors);
|
||||||
loadCondition = new ConditionVariable();
|
loadCondition = new ConditionVariable();
|
||||||
maybeFinishPrepareRunnable = this::maybeFinishPrepare;
|
maybeFinishPrepareRunnable = this::maybeFinishPrepare;
|
||||||
onContinueLoadingRequestedRunnable =
|
onContinueLoadingRequestedRunnable =
|
||||||
() -> {
|
() -> {
|
||||||
if (!released) {
|
if (!released) {
|
||||||
Assertions.checkNotNull(callback).onContinueLoadingRequested(ExtractorMediaPeriod.this);
|
Assertions.checkNotNull(callback)
|
||||||
|
.onContinueLoadingRequested(ProgressiveMediaPeriod.this);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
handler = new Handler();
|
handler = new Handler();
|
||||||
@ -356,18 +358,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
} else if (isPendingReset()) {
|
} else if (isPendingReset()) {
|
||||||
return pendingResetPositionUs;
|
return pendingResetPositionUs;
|
||||||
}
|
}
|
||||||
long largestQueuedTimestampUs;
|
long largestQueuedTimestampUs = Long.MAX_VALUE;
|
||||||
if (haveAudioVideoTracks) {
|
if (haveAudioVideoTracks) {
|
||||||
// Ignore non-AV tracks, which may be sparse or poorly interleaved.
|
// Ignore non-AV tracks, which may be sparse or poorly interleaved.
|
||||||
largestQueuedTimestampUs = Long.MAX_VALUE;
|
|
||||||
int trackCount = sampleQueues.length;
|
int trackCount = sampleQueues.length;
|
||||||
for (int i = 0; i < trackCount; i++) {
|
for (int i = 0; i < trackCount; i++) {
|
||||||
if (trackIsAudioVideoFlags[i]) {
|
if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) {
|
||||||
largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
|
largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
|
||||||
sampleQueues[i].getLargestQueuedTimestampUs());
|
sampleQueues[i].getLargestQueuedTimestampUs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
if (largestQueuedTimestampUs == Long.MAX_VALUE) {
|
||||||
largestQueuedTimestampUs = getLargestQueuedTimestampUs();
|
largestQueuedTimestampUs = getLargestQueuedTimestampUs();
|
||||||
}
|
}
|
||||||
return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs
|
return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs
|
||||||
@ -851,23 +853,23 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isReady() {
|
public boolean isReady() {
|
||||||
return ExtractorMediaPeriod.this.isReady(track);
|
return ProgressiveMediaPeriod.this.isReady(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void maybeThrowError() throws IOException {
|
public void maybeThrowError() throws IOException {
|
||||||
ExtractorMediaPeriod.this.maybeThrowError();
|
ProgressiveMediaPeriod.this.maybeThrowError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
|
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
|
||||||
boolean formatRequired) {
|
boolean formatRequired) {
|
||||||
return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired);
|
return ProgressiveMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int skipData(long positionUs) {
|
public int skipData(long positionUs) {
|
||||||
return ExtractorMediaPeriod.this.skipData(track, positionUs);
|
return ProgressiveMediaPeriod.this.skipData(track, positionUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -988,7 +990,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
position,
|
position,
|
||||||
C.LENGTH_UNSET,
|
C.LENGTH_UNSET,
|
||||||
customCacheKey,
|
customCacheKey,
|
||||||
DataSpec.FLAG_ALLOW_ICY_METADATA | DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN);
|
DataSpec.FLAG_ALLOW_ICY_METADATA
|
||||||
|
| DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN
|
||||||
|
| DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setLoadPosition(long position, long timeUs) {
|
private void setLoadPosition(long position, long timeUs) {
|
@ -0,0 +1,281 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 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.source;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||||
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||||
|
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||||
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}.
|
||||||
|
*
|
||||||
|
* <p>If the possible input stream container formats are known, pass a factory that instantiates
|
||||||
|
* extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to use
|
||||||
|
* the default extractors. When reading a new stream, the first {@link Extractor} in the array of
|
||||||
|
* extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be
|
||||||
|
* used to extract samples from the input stream.
|
||||||
|
*
|
||||||
|
* <p>Note that the built-in extractor for FLV streams does not support seeking.
|
||||||
|
*/
|
||||||
|
public final class ProgressiveMediaSource extends BaseMediaSource
|
||||||
|
implements ProgressiveMediaPeriod.Listener {
|
||||||
|
|
||||||
|
/** Factory for {@link ProgressiveMediaSource}s. */
|
||||||
|
public static final class Factory implements AdsMediaSource.MediaSourceFactory {
|
||||||
|
|
||||||
|
private final DataSource.Factory dataSourceFactory;
|
||||||
|
|
||||||
|
@Nullable private ExtractorsFactory extractorsFactory;
|
||||||
|
@Nullable private String customCacheKey;
|
||||||
|
@Nullable private Object tag;
|
||||||
|
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||||
|
private int continueLoadingCheckIntervalBytes;
|
||||||
|
private boolean isCreateCalled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new factory for {@link ProgressiveMediaSource}s.
|
||||||
|
*
|
||||||
|
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
|
||||||
|
*/
|
||||||
|
public Factory(DataSource.Factory dataSourceFactory) {
|
||||||
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
|
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
|
||||||
|
continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the factory for {@link Extractor}s to process the media stream. The default value is an
|
||||||
|
* instance of {@link DefaultExtractorsFactory}.
|
||||||
|
*
|
||||||
|
* @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
|
||||||
|
* possible formats are known, pass a factory that instantiates extractors for those
|
||||||
|
* formats.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||||
|
*/
|
||||||
|
public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) {
|
||||||
|
Assertions.checkState(!isCreateCalled);
|
||||||
|
this.extractorsFactory = extractorsFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the custom key that uniquely identifies the original stream. Used for cache indexing.
|
||||||
|
* The default value is {@code null}.
|
||||||
|
*
|
||||||
|
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for
|
||||||
|
* cache indexing.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||||
|
*/
|
||||||
|
public Factory setCustomCacheKey(String customCacheKey) {
|
||||||
|
Assertions.checkState(!isCreateCalled);
|
||||||
|
this.customCacheKey = customCacheKey;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a tag for the media source which will be published in the {@link
|
||||||
|
* com.google.android.exoplayer2.Timeline} of the source as {@link
|
||||||
|
* com.google.android.exoplayer2.Timeline.Window#tag}.
|
||||||
|
*
|
||||||
|
* @param tag A tag for the media source.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||||
|
*/
|
||||||
|
public Factory setTag(Object tag) {
|
||||||
|
Assertions.checkState(!isCreateCalled);
|
||||||
|
this.tag = tag;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link
|
||||||
|
* DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}.
|
||||||
|
*
|
||||||
|
* @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||||
|
*/
|
||||||
|
public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
|
||||||
|
Assertions.checkState(!isCreateCalled);
|
||||||
|
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the number of bytes that should be loaded between each invocation of {@link
|
||||||
|
* MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is
|
||||||
|
* {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}.
|
||||||
|
*
|
||||||
|
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between
|
||||||
|
* each invocation of {@link
|
||||||
|
* MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||||
|
*/
|
||||||
|
public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) {
|
||||||
|
Assertions.checkState(!isCreateCalled);
|
||||||
|
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@link ProgressiveMediaSource} using the current parameters.
|
||||||
|
*
|
||||||
|
* @param uri The {@link Uri}.
|
||||||
|
* @return The new {@link ProgressiveMediaSource}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ProgressiveMediaSource createMediaSource(Uri uri) {
|
||||||
|
isCreateCalled = true;
|
||||||
|
if (extractorsFactory == null) {
|
||||||
|
extractorsFactory = new DefaultExtractorsFactory();
|
||||||
|
}
|
||||||
|
return new ProgressiveMediaSource(
|
||||||
|
uri,
|
||||||
|
dataSourceFactory,
|
||||||
|
extractorsFactory,
|
||||||
|
loadErrorHandlingPolicy,
|
||||||
|
customCacheKey,
|
||||||
|
continueLoadingCheckIntervalBytes,
|
||||||
|
tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getSupportedTypes() {
|
||||||
|
return new int[] {C.TYPE_OTHER};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default number of bytes that should be loaded between each each invocation of {@link
|
||||||
|
* MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
|
||||||
|
*/
|
||||||
|
public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;
|
||||||
|
|
||||||
|
private final Uri uri;
|
||||||
|
private final DataSource.Factory dataSourceFactory;
|
||||||
|
private final ExtractorsFactory extractorsFactory;
|
||||||
|
private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;
|
||||||
|
@Nullable private final String customCacheKey;
|
||||||
|
private final int continueLoadingCheckIntervalBytes;
|
||||||
|
@Nullable private final Object tag;
|
||||||
|
|
||||||
|
private long timelineDurationUs;
|
||||||
|
private boolean timelineIsSeekable;
|
||||||
|
@Nullable private TransferListener transferListener;
|
||||||
|
|
||||||
|
// TODO: Make private when ExtractorMediaSource is deleted.
|
||||||
|
/* package */ ProgressiveMediaSource(
|
||||||
|
Uri uri,
|
||||||
|
DataSource.Factory dataSourceFactory,
|
||||||
|
ExtractorsFactory extractorsFactory,
|
||||||
|
LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy,
|
||||||
|
@Nullable String customCacheKey,
|
||||||
|
int continueLoadingCheckIntervalBytes,
|
||||||
|
@Nullable Object tag) {
|
||||||
|
this.uri = uri;
|
||||||
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
|
this.extractorsFactory = extractorsFactory;
|
||||||
|
this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy;
|
||||||
|
this.customCacheKey = customCacheKey;
|
||||||
|
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
||||||
|
this.timelineDurationUs = C.TIME_UNSET;
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public Object getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
|
transferListener = mediaTransferListener;
|
||||||
|
notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||||
|
DataSource dataSource = dataSourceFactory.createDataSource();
|
||||||
|
if (transferListener != null) {
|
||||||
|
dataSource.addTransferListener(transferListener);
|
||||||
|
}
|
||||||
|
return new ProgressiveMediaPeriod(
|
||||||
|
uri,
|
||||||
|
dataSource,
|
||||||
|
extractorsFactory.createExtractors(),
|
||||||
|
loadableLoadErrorHandlingPolicy,
|
||||||
|
createEventDispatcher(id),
|
||||||
|
this,
|
||||||
|
allocator,
|
||||||
|
customCacheKey,
|
||||||
|
continueLoadingCheckIntervalBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||||
|
((ProgressiveMediaPeriod) mediaPeriod).release();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releaseSourceInternal() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProgressiveMediaPeriod.Listener implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) {
|
||||||
|
// If we already have the duration from a previous source info refresh, use it.
|
||||||
|
durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs;
|
||||||
|
if (timelineDurationUs == durationUs && timelineIsSeekable == isSeekable) {
|
||||||
|
// Suppress no-op source info changes.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifySourceInfoRefreshed(durationUs, isSeekable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods.
|
||||||
|
|
||||||
|
private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) {
|
||||||
|
timelineDurationUs = durationUs;
|
||||||
|
timelineIsSeekable = isSeekable;
|
||||||
|
// TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223.
|
||||||
|
refreshSourceInfo(
|
||||||
|
new SinglePeriodTimeline(
|
||||||
|
timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, tag),
|
||||||
|
/* manifest= */ null);
|
||||||
|
}
|
||||||
|
}
|
@ -57,6 +57,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
|
|
||||||
private long largestDiscardedTimestampUs;
|
private long largestDiscardedTimestampUs;
|
||||||
private long largestQueuedTimestampUs;
|
private long largestQueuedTimestampUs;
|
||||||
|
private boolean isLastSampleQueued;
|
||||||
private boolean upstreamKeyframeRequired;
|
private boolean upstreamKeyframeRequired;
|
||||||
private boolean upstreamFormatRequired;
|
private boolean upstreamFormatRequired;
|
||||||
private Format upstreamFormat;
|
private Format upstreamFormat;
|
||||||
@ -93,6 +94,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
upstreamKeyframeRequired = true;
|
upstreamKeyframeRequired = true;
|
||||||
largestDiscardedTimestampUs = Long.MIN_VALUE;
|
largestDiscardedTimestampUs = Long.MIN_VALUE;
|
||||||
largestQueuedTimestampUs = Long.MIN_VALUE;
|
largestQueuedTimestampUs = Long.MIN_VALUE;
|
||||||
|
isLastSampleQueued = false;
|
||||||
if (resetUpstreamFormat) {
|
if (resetUpstreamFormat) {
|
||||||
upstreamFormat = null;
|
upstreamFormat = null;
|
||||||
upstreamFormatRequired = true;
|
upstreamFormatRequired = true;
|
||||||
@ -118,6 +120,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition));
|
Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition));
|
||||||
length -= discardCount;
|
length -= discardCount;
|
||||||
largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length));
|
largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length));
|
||||||
|
isLastSampleQueued = discardCount == 0 && isLastSampleQueued;
|
||||||
if (length == 0) {
|
if (length == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
@ -186,6 +189,19 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
return largestQueuedTimestampUs;
|
return largestQueuedTimestampUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the last sample of the stream has knowingly been queued. A return value of
|
||||||
|
* {@code false} means that the last sample had not been queued or that it's unknown whether the
|
||||||
|
* last sample has been queued.
|
||||||
|
*
|
||||||
|
* <p>Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
|
||||||
|
* considered as having been queued. Samples that were dequeued from the front of the queue are
|
||||||
|
* considered as having been queued.
|
||||||
|
*/
|
||||||
|
public synchronized boolean isLastSampleQueued() {
|
||||||
|
return isLastSampleQueued;
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
|
/** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
|
||||||
public synchronized long getFirstTimestampUs() {
|
public synchronized long getFirstTimestampUs() {
|
||||||
return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex];
|
return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex];
|
||||||
@ -224,7 +240,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
|
boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
|
||||||
SampleExtrasHolder extrasHolder) {
|
SampleExtrasHolder extrasHolder) {
|
||||||
if (!hasNextSample()) {
|
if (!hasNextSample()) {
|
||||||
if (loadingFinished) {
|
if (loadingFinished || isLastSampleQueued) {
|
||||||
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||||
return C.RESULT_BUFFER_READ;
|
return C.RESULT_BUFFER_READ;
|
||||||
} else if (upstreamFormat != null
|
} else if (upstreamFormat != null
|
||||||
@ -388,7 +404,9 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
upstreamKeyframeRequired = false;
|
upstreamKeyframeRequired = false;
|
||||||
}
|
}
|
||||||
Assertions.checkState(!upstreamFormatRequired);
|
Assertions.checkState(!upstreamFormatRequired);
|
||||||
commitSampleTimestamp(timeUs);
|
|
||||||
|
isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0;
|
||||||
|
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
|
||||||
|
|
||||||
int relativeEndIndex = getRelativeIndex(length);
|
int relativeEndIndex = getRelativeIndex(length);
|
||||||
timesUs[relativeEndIndex] = timeUs;
|
timesUs[relativeEndIndex] = timeUs;
|
||||||
@ -439,10 +457,6 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void commitSampleTimestamp(long timeUs) {
|
|
||||||
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to discard samples from the end of the queue to allow samples starting from the
|
* Attempts to discard samples from the end of the queue to allow samples starting from the
|
||||||
* specified timestamp to be spliced in. Samples will not be discarded prior to the read position.
|
* specified timestamp to be spliced in. Samples will not be discarded prior to the read position.
|
||||||
|
@ -224,6 +224,15 @@ public class SampleQueue implements TrackOutput {
|
|||||||
return metadataQueue.getLargestQueuedTimestampUs();
|
return metadataQueue.getLargestQueuedTimestampUs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the last sample of the stream has knowingly been queued. A return value of
|
||||||
|
* {@code false} means that the last sample had not been queued or that it's unknown whether the
|
||||||
|
* last sample has been queued.
|
||||||
|
*/
|
||||||
|
public boolean isLastSampleQueued() {
|
||||||
|
return metadataQueue.isLastSampleQueued();
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
|
/** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
|
||||||
public long getFirstTimestampUs() {
|
public long getFirstTimestampUs() {
|
||||||
return metadataQueue.getFirstTimestampUs();
|
return metadataQueue.getFirstTimestampUs();
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user