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-project.txt
|
||||
|
||||
# Bazel
|
||||
bazel-bin
|
||||
bazel-genfiles
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
|
||||
# Other
|
||||
.DS_Store
|
||||
cmake-build-debug
|
||||
@ -66,3 +72,6 @@ extensions/cronet/jniLibs/*
|
||||
extensions/cronet/libs/*
|
||||
!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-project.txt
|
||||
|
||||
# Bazel
|
||||
bazel-bin
|
||||
bazel-genfiles
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
|
||||
# Other
|
||||
.DS_Store
|
||||
cmake-build-debug
|
||||
@ -69,3 +75,7 @@ extensions/cronet/jniLibs/*
|
||||
!extensions/cronet/jniLibs/README.md
|
||||
extensions/cronet/libs/*
|
||||
!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 ###
|
||||
|
||||
#### 1. Add repositories ####
|
||||
|
||||
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
|
||||
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
|
||||
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'
|
||||
```
|
||||
|
||||
where `2.X.X` is your preferred version. 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
|
||||
}
|
||||
```
|
||||
where `2.X.X` is your preferred version.
|
||||
|
||||
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
|
||||
@ -87,6 +83,32 @@ JCenter can be found on [Bintray][].
|
||||
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
|
||||
[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 ###
|
||||
|
||||
Cloning the repository and depending on the modules locally is required when
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
### dev-v2 (not yet released) ###
|
||||
|
||||
* `ExtractorMediaSource` renamed to `ProgressiveMediaSource`.
|
||||
* Support for playing spherical videos on Daydream.
|
||||
* Improve decoder re-use between playbacks. TODO: Write and link a blog post
|
||||
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)).
|
||||
@ -17,7 +18,8 @@
|
||||
* Add `setStreamKeys` method to factories of DASH, SmoothStreaming and HLS
|
||||
media sources to simplify filtering by downloaded streams.
|
||||
* 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
|
||||
this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
|
||||
replaced with an opt out flag
|
||||
@ -27,20 +29,70 @@
|
||||
* Rename TaskState to DownloadState.
|
||||
* Add new states to DownloadState.
|
||||
* 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
|
||||
([#3735](https://github.com/google/ExoPlayer/issues/3735)).
|
||||
* IMA extension:
|
||||
* Clear ads loader listeners on release
|
||||
([#4114](https://github.com/google/ExoPlayer/issues/4114)).
|
||||
* Require setting the `Player` on `AdsLoader` instances before playback.
|
||||
* CEA-608: Improved conformance to the specification
|
||||
([#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)).
|
||||
* 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
|
||||
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
|
||||
because of parallel player access
|
||||
([#5240](https://github.com/google/ExoPlayer/issues/5240)).
|
||||
* Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a
|
||||
callback `Runnable`.
|
||||
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.
|
||||
* Fix issue with reusing a `ClippingMediaSource` with an inner
|
||||
`ExtractorMediaSource` and a non-zero start position
|
||||
([#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 ###
|
||||
|
||||
@ -1173,7 +1225,7 @@
|
||||
[here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi).
|
||||
* Robustness improvements when handling MediaSource timeline changes and
|
||||
MediaPeriod transitions.
|
||||
* EIA608: Support for caption styling and positioning.
|
||||
* CEA-608: Support for caption styling and positioning.
|
||||
* MPEG-TS: Improved support:
|
||||
* Support injection of custom TS payload readers.
|
||||
* Support injection of custom section payload readers.
|
||||
@ -1417,8 +1469,8 @@ V2 release.
|
||||
(#801).
|
||||
* MP3: Fix playback of some streams when stream length is unknown.
|
||||
* ID3: Support multiple frames of the same type in a single tag.
|
||||
* EIA608: Correctly handle repeated control characters, fixing an issue in which
|
||||
captions would immediately disappear.
|
||||
* CEA-608: Correctly handle repeated control characters, fixing an issue in
|
||||
which captions would immediately disappear.
|
||||
* 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.
|
||||
* Misc bug fixes.
|
||||
|
@ -13,13 +13,9 @@
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.9.3'
|
||||
releaseVersionCode = 2009003
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
||||
// 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
|
||||
releaseVersion = '2.9.5'
|
||||
releaseVersionCode = 2009005
|
||||
minSdkVersion = 16
|
||||
targetSdkVersion = 28
|
||||
compileSdkVersion = 28
|
||||
buildToolsVersion = '28.0.2'
|
||||
|
@ -26,7 +26,7 @@ android {
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 16
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
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.SessionAvailabilityListener;
|
||||
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.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
@ -63,7 +63,7 @@ import java.util.ArrayList;
|
||||
private final SimpleExoPlayer exoPlayer;
|
||||
private final CastPlayer castPlayer;
|
||||
private final ArrayList<MediaItem> mediaQueue;
|
||||
private final QueuePositionListener queuePositionListener;
|
||||
private final QueueChangesListener queueChangesListener;
|
||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||
|
||||
private boolean castMediaQueueCreationPending;
|
||||
@ -71,32 +71,21 @@ import java.util.ArrayList;
|
||||
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 castControlView The {@link PlayerControlView} to control remote playback.
|
||||
* @param context A {@link Context}.
|
||||
* @param castContext The {@link CastContext}.
|
||||
*/
|
||||
public static DefaultReceiverPlayerManager createPlayerManager(
|
||||
QueuePositionListener queuePositionListener,
|
||||
public DefaultReceiverPlayerManager(
|
||||
QueueChangesListener queueChangesListener,
|
||||
PlayerView localPlayerView,
|
||||
PlayerControlView castControlView,
|
||||
Context context,
|
||||
CastContext castContext) {
|
||||
DefaultReceiverPlayerManager defaultReceiverPlayerManager =
|
||||
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.queueChangesListener = queueChangesListener;
|
||||
this.localPlayerView = localPlayerView;
|
||||
this.castControlView = castControlView;
|
||||
mediaQueue = new ArrayList<>();
|
||||
@ -113,6 +102,8 @@ import java.util.ArrayList;
|
||||
castPlayer.addListener(this);
|
||||
castPlayer.setSessionAvailabilityListener(this);
|
||||
castControlView.setPlayer(castPlayer);
|
||||
|
||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||
}
|
||||
|
||||
// Queue manipulation methods.
|
||||
@ -287,10 +278,6 @@ import java.util.ArrayList;
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void init() {
|
||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||
}
|
||||
|
||||
private void updateCurrentItemIndex() {
|
||||
int playbackState = currentPlayer.getPlaybackState();
|
||||
maybeSetCurrentItemAndNotify(
|
||||
@ -372,7 +359,7 @@ import java.util.ArrayList;
|
||||
if (this.currentItemIndex != currentItemIndex) {
|
||||
int oldIndex = this.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:
|
||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
||||
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: {
|
||||
throw new IllegalStateException("Unsupported type: " + item.mimeType);
|
||||
}
|
||||
|
@ -15,10 +15,14 @@
|
||||
*/
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Utility methods and constants for the Cast demo application. */
|
||||
/* package */ final class DemoUtil {
|
||||
@ -32,6 +36,16 @@ import java.util.List;
|
||||
public final String name;
|
||||
/** The mime type of the sample media content. */
|
||||
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}.
|
||||
@ -39,9 +53,21 @@ import java.util.List;
|
||||
* @param mimeType See {@link #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.name = name;
|
||||
this.mimeType = mimeType;
|
||||
this.drmSchemeUuid = drmSchemeUuid;
|
||||
this.licenseServerUri =
|
||||
licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -62,25 +88,15 @@ import java.util.List;
|
||||
// App samples.
|
||||
ArrayList<Sample> samples = new ArrayList<>();
|
||||
|
||||
// Clear content.
|
||||
samples.add(
|
||||
new Sample(
|
||||
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
||||
"DASH (clear,MP4,H264)",
|
||||
"Clear DASH: Tears",
|
||||
MIME_TYPE_DASH));
|
||||
samples.add(
|
||||
new Sample(
|
||||
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
|
||||
+ "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));
|
||||
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4));
|
||||
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.CastContext;
|
||||
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
|
||||
* Cast extension.
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity
|
||||
implements OnClickListener, PlayerManager.QueuePositionListener {
|
||||
implements OnClickListener, PlayerManager.QueueChangesListener {
|
||||
|
||||
private final MediaItem.Builder mediaItemBuilder;
|
||||
|
||||
@ -120,8 +121,8 @@ public class MainActivity extends AppCompatActivity
|
||||
switch (applicationId) {
|
||||
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
|
||||
playerManager =
|
||||
DefaultReceiverPlayerManager.createPlayerManager(
|
||||
/* queuePositionListener= */ this,
|
||||
new DefaultReceiverPlayerManager(
|
||||
/* queueChangesListener= */ this,
|
||||
localPlayerView,
|
||||
castControlView,
|
||||
/* context= */ this,
|
||||
@ -161,7 +162,7 @@ public class MainActivity extends AppCompatActivity
|
||||
.show();
|
||||
}
|
||||
|
||||
// PlayerManager.QueuePositionListener implementation.
|
||||
// PlayerManager.QueueChangesListener implementation.
|
||||
|
||||
@Override
|
||||
public void onQueuePositionChanged(int previousIndex, int newIndex) {
|
||||
@ -173,6 +174,11 @@ public class MainActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueueContentsExternallyChanged() {
|
||||
mediaQueueListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private View buildSampleListView() {
|
||||
@ -182,13 +188,18 @@ public class MainActivity extends AppCompatActivity
|
||||
sampleList.setOnItemClickListener(
|
||||
(parent, view, position, id) -> {
|
||||
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position);
|
||||
playerManager.addItem(
|
||||
mediaItemBuilder
|
||||
.clear()
|
||||
.setMedia(sample.uri)
|
||||
.setTitle(sample.name)
|
||||
.setMimeType(sample.mimeType)
|
||||
.build());
|
||||
mediaItemBuilder
|
||||
.clear()
|
||||
.setMedia(sample.uri)
|
||||
.setTitle(sample.name)
|
||||
.setMimeType(sample.mimeType);
|
||||
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);
|
||||
});
|
||||
return dialogList;
|
||||
@ -268,6 +279,8 @@ public class MainActivity extends AppCompatActivity
|
||||
int position = viewHolder.getAdapterPosition();
|
||||
if (playerManager.removeItem(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. */
|
||||
interface PlayerManager {
|
||||
|
||||
/** Listener for changes in the media queue playback position. */
|
||||
interface QueuePositionListener {
|
||||
/** Listener for changes in the media queue. */
|
||||
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);
|
||||
|
||||
/** 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. */
|
||||
|
@ -26,7 +26,7 @@ android {
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 16
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
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.SimpleExoPlayer;
|
||||
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.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
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.upstream.DataSource;
|
||||
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) {
|
||||
// Create a default track selector.
|
||||
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
|
||||
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
|
||||
|
||||
// Create a player instance.
|
||||
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
|
||||
|
||||
// Bind the player to the view.
|
||||
player = ExoPlayerFactory.newSimpleInstance(context);
|
||||
adsLoader.setPlayer(player);
|
||||
playerView.setPlayer(player);
|
||||
|
||||
// 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();
|
||||
player.release();
|
||||
player = null;
|
||||
adsLoader.setPlayer(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,7 +117,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ android {
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 16
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,11 @@ package com.google.android.exoplayer2.demo;
|
||||
import android.app.Application;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
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.DownloadIndexUtil;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
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.NoOpCacheEvictor;
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
||||
*/
|
||||
public class DemoApplication extends Application {
|
||||
|
||||
private static final String TAG = "DemoApplication";
|
||||
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
||||
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||
@ -97,19 +104,28 @@ public class DemoApplication extends Application {
|
||||
|
||||
private synchronized void initDownloadManager() {
|
||||
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 =
|
||||
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
this,
|
||||
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
|
||||
new DefaultDownloaderFactory(downloaderConstructorHelper),
|
||||
MAX_SIMULTANEOUS_DOWNLOADS,
|
||||
DownloadManager.DEFAULT_MIN_RETRY_COUNT);
|
||||
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
|
||||
DownloadManager.DEFAULT_REQUIREMENTS);
|
||||
downloadTracker =
|
||||
new DownloadTracker(
|
||||
/* context= */ this,
|
||||
buildDataSourceFactory(),
|
||||
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
|
||||
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadIndex);
|
||||
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.DownloadState;
|
||||
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.Util;
|
||||
|
||||
@ -33,6 +33,8 @@ public class DemoDownloadService extends DownloadService {
|
||||
|
||||
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||
|
||||
private DownloadNotificationHelper notificationHelper;
|
||||
|
||||
public DemoDownloadService() {
|
||||
super(
|
||||
FOREGROUND_NOTIFICATION_ID,
|
||||
@ -42,6 +44,12 @@ public class DemoDownloadService extends DownloadService {
|
||||
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DownloadManager getDownloadManager() {
|
||||
return ((DemoApplication) getApplication()).getDownloadManager();
|
||||
@ -54,32 +62,23 @@ public class DemoDownloadService extends DownloadService {
|
||||
|
||||
@Override
|
||||
protected Notification getForegroundNotification(DownloadState[] downloadStates) {
|
||||
return DownloadNotificationUtil.buildProgressNotification(
|
||||
/* context= */ this,
|
||||
R.drawable.ic_download,
|
||||
CHANNEL_ID,
|
||||
/* contentIntent= */ null,
|
||||
/* message= */ null,
|
||||
downloadStates);
|
||||
return notificationHelper.buildProgressNotification(
|
||||
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloadStates);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDownloadStateChanged(DownloadState downloadState) {
|
||||
Notification notification = null;
|
||||
Notification notification;
|
||||
if (downloadState.state == DownloadState.STATE_COMPLETED) {
|
||||
notification =
|
||||
DownloadNotificationUtil.buildDownloadCompletedNotification(
|
||||
/* context= */ this,
|
||||
notificationHelper.buildDownloadCompletedNotification(
|
||||
R.drawable.ic_download_done,
|
||||
CHANNEL_ID,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(downloadState.customMetadata));
|
||||
} else if (downloadState.state == DownloadState.STATE_FAILED) {
|
||||
notification =
|
||||
DownloadNotificationUtil.buildDownloadFailedNotification(
|
||||
/* context= */ this,
|
||||
notificationHelper.buildDownloadFailedNotification(
|
||||
R.drawable.ic_download_done,
|
||||
CHANNEL_ID,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(downloadState.customMetadata));
|
||||
} else {
|
||||
|
@ -34,17 +34,16 @@ import android.widget.Toast;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
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.DownloadHelper;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
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.scheduler.Requirements;
|
||||
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.MappingTrackSelector.MappedTrackInfo;
|
||||
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.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -83,20 +82,21 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final TrackNameProvider trackNameProvider;
|
||||
private final CopyOnWriteArraySet<Listener> listeners;
|
||||
private final HashMap<Uri, DownloadAction> trackedDownloadStates;
|
||||
private final ActionFile actionFile;
|
||||
private final Handler actionFileWriteHandler;
|
||||
private final HashMap<Uri, DownloadState> trackedDownloadStates;
|
||||
private final DefaultDownloadIndex downloadIndex;
|
||||
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.dataSourceFactory = dataSourceFactory;
|
||||
this.actionFile = new ActionFile(actionFile);
|
||||
this.downloadIndex = downloadIndex;
|
||||
trackNameProvider = new DefaultTrackNameProvider(context.getResources());
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
trackedDownloadStates = new HashMap<>();
|
||||
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
|
||||
actionFileWriteThread.start();
|
||||
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
|
||||
actionFileIOHandler = new Handler(actionFileWriteThread.getLooper());
|
||||
loadTrackedActions();
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||
if (!trackedDownloadStates.containsKey(uri)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return trackedDownloadStates.get(uri).getKeys();
|
||||
return Arrays.asList(trackedDownloadStates.get(uri).streamKeys);
|
||||
}
|
||||
|
||||
public void toggleDownload(
|
||||
@ -149,7 +149,7 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||
|| downloadState.state == DownloadState.STATE_FAILED) {
|
||||
// A download has been removed, or has failed. Stop tracking it.
|
||||
if (trackedDownloadStates.remove(downloadState.uri) != null) {
|
||||
handleTrackedDownloadStatesChanged();
|
||||
handleTrackedDownloadStateChanged(downloadState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,30 +159,35 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequirementsStateChanged(
|
||||
DownloadManager downloadManager,
|
||||
Requirements requirements,
|
||||
@Requirements.RequirementFlags int notMetRequirements) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
private void loadTrackedActions() {
|
||||
try {
|
||||
DownloadAction[] allActions = actionFile.load();
|
||||
for (DownloadAction action : allActions) {
|
||||
trackedDownloadStates.put(action.uri, action);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to load tracked actions", e);
|
||||
DownloadStateCursor downloadStates = downloadIndex.getDownloadStates();
|
||||
while (downloadStates.moveToNext()) {
|
||||
DownloadState downloadState = downloadStates.getDownloadState();
|
||||
trackedDownloadStates.put(downloadState.uri, downloadState);
|
||||
}
|
||||
downloadStates.close();
|
||||
}
|
||||
|
||||
private void handleTrackedDownloadStatesChanged() {
|
||||
private void handleTrackedDownloadStateChanged(DownloadState downloadState) {
|
||||
for (Listener listener : listeners) {
|
||||
listener.onDownloadsChanged();
|
||||
}
|
||||
final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]);
|
||||
actionFileWriteHandler.post(
|
||||
actionFileIOHandler.post(
|
||||
() -> {
|
||||
try {
|
||||
actionFile.store(actions);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to store tracked actions", e);
|
||||
if (downloadState.state == DownloadState.STATE_REMOVED) {
|
||||
downloadIndex.removeDownloadState(downloadState.id);
|
||||
} else {
|
||||
downloadIndex.putDownloadState(downloadState);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -192,8 +197,9 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||
// This content is already being downloaded. Do nothing.
|
||||
return;
|
||||
}
|
||||
trackedDownloadStates.put(action.uri, action);
|
||||
handleTrackedDownloadStatesChanged();
|
||||
DownloadState downloadState = new DownloadState(action);
|
||||
trackedDownloadStates.put(downloadState.uri, downloadState);
|
||||
handleTrackedDownloadStateChanged(downloadState);
|
||||
startServiceWithAction(action);
|
||||
}
|
||||
|
||||
@ -201,18 +207,18 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||
DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
|
||||
}
|
||||
|
||||
private DownloadHelper<?> getDownloadHelper(
|
||||
private DownloadHelper getDownloadHelper(
|
||||
Uri uri, String extension, RenderersFactory renderersFactory) {
|
||||
int type = Util.inferContentType(uri, extension);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashDownloadHelper(uri, dataSourceFactory, renderersFactory);
|
||||
return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_SS:
|
||||
return new SsDownloadHelper(uri, dataSourceFactory, renderersFactory);
|
||||
return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsDownloadHelper(uri, dataSourceFactory, renderersFactory);
|
||||
return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_OTHER:
|
||||
return new ProgressiveDownloadHelper(uri);
|
||||
return DownloadHelper.forProgressive(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
@ -222,10 +228,11 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||
private final class StartDownloadDialogHelper
|
||||
implements DownloadHelper.Callback,
|
||||
DialogInterface.OnClickListener,
|
||||
DialogInterface.OnDismissListener,
|
||||
View.OnClickListener,
|
||||
TrackSelectionView.DialogCallback {
|
||||
|
||||
private final DownloadHelper<?> downloadHelper;
|
||||
private final DownloadHelper downloadHelper;
|
||||
private final String name;
|
||||
private final LayoutInflater dialogInflater;
|
||||
private final AlertDialog dialog;
|
||||
@ -235,20 +242,21 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||
private DefaultTrackSelector.Parameters parameters;
|
||||
|
||||
private StartDownloadDialogHelper(
|
||||
Activity activity, DownloadHelper<?> downloadHelper, String name) {
|
||||
Activity activity, DownloadHelper downloadHelper, String name) {
|
||||
this.downloadHelper = downloadHelper;
|
||||
this.name = name;
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.download_preparing)
|
||||
.setPositiveButton(android.R.string.ok, this)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
.setPositiveButton(android.R.string.ok, /* listener= */ this)
|
||||
.setNegativeButton(android.R.string.cancel, /* listener= */ null);
|
||||
|
||||
// Inflate with the builder's context to ensure the correct style is used.
|
||||
dialogInflater = LayoutInflater.from(builder.getContext());
|
||||
selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null);
|
||||
builder.setView(selectionList);
|
||||
dialog = builder.create();
|
||||
dialog.setOnDismissListener(/* listener= */ this);
|
||||
dialog.show();
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
|
||||
@ -259,19 +267,17 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||
// DownloadHelper.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void onPrepared(DownloadHelper<?> helper) {
|
||||
if (helper.getPeriodCount() < 1) {
|
||||
onPrepareError(downloadHelper, new IOException("Content is empty."));
|
||||
return;
|
||||
public void onPrepared(DownloadHelper helper) {
|
||||
if (helper.getPeriodCount() > 0) {
|
||||
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||
updateSelectionList();
|
||||
}
|
||||
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||
updateSelectionList();
|
||||
dialog.setTitle(R.string.exo_download_description);
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareError(DownloadHelper<?> helper, IOException e) {
|
||||
public void onPrepareError(DownloadHelper helper, IOException e) {
|
||||
Toast.makeText(
|
||||
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
@ -317,6 +323,13 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||
startDownload(downloadAction);
|
||||
}
|
||||
|
||||
// DialogInterface.OnDismissListener implementation.
|
||||
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
downloadHelper.release();
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
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.source.BehindLiveWindowException;
|
||||
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.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
@ -483,7 +483,7 @@ public class PlayerActivity extends Activity
|
||||
.setStreamKeys(offlineStreamKeys)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
default: {
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ android {
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ android {
|
||||
}
|
||||
|
||||
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 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
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.Renderer;
|
||||
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.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import org.junit.Before;
|
||||
@ -86,7 +86,7 @@ public class FlacPlaybackTest {
|
||||
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
|
||||
player.addListener(this);
|
||||
MediaSource mediaSource =
|
||||
new ExtractorMediaSource.Factory(
|
||||
new ProgressiveMediaSource.Factory(
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
|
||||
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
||||
.createMediaSource(uri);
|
||||
|
@ -33,9 +33,7 @@ dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation 'com.google.vr:sdk-audio:1.80.0'
|
||||
implementation 'com.google.vr:sdk-controller:1.80.0'
|
||||
api 'com.google.vr:sdk-base:1.80.0'
|
||||
api 'com.google.vr:sdk-base:1.190.0'
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
}
|
||||
|
||||
|
@ -31,13 +31,13 @@ android {
|
||||
}
|
||||
|
||||
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 '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
|
||||
// com.android.support:support-v4 and com.android.support:customtabs to be
|
||||
// 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
|
||||
implementation 'com.android.support:support-v4:' + supportLibraryVersion
|
||||
implementation 'com.android.support:customtabs:' + supportLibraryVersion
|
||||
|
@ -466,11 +466,11 @@ public final class ImaAdsLoader
|
||||
}
|
||||
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
|
||||
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
|
||||
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings);
|
||||
period = new Timeline.Period();
|
||||
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
|
||||
adDisplayContainer = imaFactory.createAdDisplayContainer();
|
||||
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
|
||||
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
|
||||
adsLoader.addAdErrorListener(/* adErrorListener= */ this);
|
||||
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
|
||||
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
||||
@ -524,7 +524,6 @@ public final class ImaAdsLoader
|
||||
if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
|
||||
request.setVastLoadTimeout(vastLoadTimeoutMs);
|
||||
}
|
||||
request.setAdDisplayContainer(adDisplayContainer);
|
||||
request.setContentProgressProvider(this);
|
||||
request.setUserRequestContext(pendingAdRequestContext);
|
||||
adsLoader.requestAds(request);
|
||||
@ -1374,9 +1373,9 @@ public final class ImaAdsLoader
|
||||
AdDisplayContainer createAdDisplayContainer();
|
||||
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */
|
||||
AdsRequest createAdsRequest();
|
||||
/** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings) */
|
||||
/** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */
|
||||
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}. */
|
||||
@ -1403,8 +1402,9 @@ public final class ImaAdsLoader
|
||||
|
||||
@Override
|
||||
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
|
||||
Context context, ImaSdkSettings imaSdkSettings) {
|
||||
return ImaSdkFactory.getInstance().createAdsLoader(context, imaSdkSettings);
|
||||
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
|
||||
return ImaSdkFactory.getInstance()
|
||||
.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,8 +93,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
return adsMediaSource.createPeriod(id, allocator);
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
return adsMediaSource.createPeriod(id, allocator, startPositionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.ima;
|
||||
|
||||
import android.content.Context;
|
||||
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.AdsRequest;
|
||||
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
|
||||
@ -64,8 +65,8 @@ final class SingletonImaFactory implements ImaAdsLoader.ImaFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
|
||||
Context context, ImaSdkSettings imaSdkSettings) {
|
||||
public AdsLoader createAdsLoader(
|
||||
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
|
||||
return adsLoader;
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
api 'com.squareup.okhttp3:okhttp:3.11.0'
|
||||
api 'com.squareup.okhttp3:okhttp:3.12.1'
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -28,8 +28,8 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
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.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import org.junit.Before;
|
||||
@ -86,7 +86,7 @@ public class OpusPlaybackTest {
|
||||
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
|
||||
player.addListener(this);
|
||||
MediaSource mediaSource =
|
||||
new ExtractorMediaSource.Factory(
|
||||
new ProgressiveMediaSource.Factory(
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"))
|
||||
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
||||
.createMediaSource(uri);
|
||||
|
@ -24,7 +24,7 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 15
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
}
|
||||
|
@ -34,26 +34,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
|
||||
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:
|
||||
|
||||
```
|
||||
@ -78,10 +58,6 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||
* Android config scripts should be re-generated by running
|
||||
`generate_libvpx_android_configs.sh`
|
||||
* 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 ##
|
||||
|
||||
|
@ -29,8 +29,8 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
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.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
@ -114,12 +114,12 @@ public class VpxPlaybackTest {
|
||||
@Override
|
||||
public void run() {
|
||||
Looper.prepare();
|
||||
LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0);
|
||||
LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0);
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||
player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector);
|
||||
player.addListener(this);
|
||||
MediaSource mediaSource =
|
||||
new ExtractorMediaSource.Factory(
|
||||
new ProgressiveMediaSource.Factory(
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"))
|
||||
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
||||
.createMediaSource(uri);
|
||||
|
@ -15,8 +15,6 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.vp9;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
@ -109,7 +107,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||
/** The default input buffer size. */
|
||||
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 long allowedJoiningTimeMs;
|
||||
private final int maxDroppedFramesToNotify;
|
||||
@ -119,7 +116,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||
private final TimedValueQueue<Format> formatQueue;
|
||||
private final DecoderInputBuffer flagsOnlyBuffer;
|
||||
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||
private final boolean useSurfaceYuvOutput;
|
||||
|
||||
private Format format;
|
||||
private Format pendingFormat;
|
||||
@ -127,13 +123,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||
private VpxDecoder decoder;
|
||||
private VpxInputBuffer inputBuffer;
|
||||
private VpxOutputBuffer outputBuffer;
|
||||
private DrmSession<ExoMediaCrypto> drmSession;
|
||||
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
||||
@Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
|
||||
@Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;
|
||||
|
||||
private @ReinitializationState int decoderReinitializationState;
|
||||
private boolean decoderReceivedBuffers;
|
||||
|
||||
private Bitmap bitmap;
|
||||
private boolean renderedFirstFrame;
|
||||
private long initialPositionUs;
|
||||
private long joiningDeadlineMs;
|
||||
@ -158,16 +153,14 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||
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
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
*/
|
||||
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs) {
|
||||
this(scaleToFit, allowedJoiningTimeMs, null, null, 0);
|
||||
public LibvpxVideoRenderer(long allowedJoiningTimeMs) {
|
||||
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
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
* @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
|
||||
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
|
||||
*/
|
||||
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
|
||||
Handler eventHandler, VideoRendererEventListener eventListener,
|
||||
public LibvpxVideoRenderer(
|
||||
long allowedJoiningTimeMs,
|
||||
Handler eventHandler,
|
||||
VideoRendererEventListener eventListener,
|
||||
int maxDroppedFramesToNotify) {
|
||||
this(
|
||||
scaleToFit,
|
||||
allowedJoiningTimeMs,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
maxDroppedFramesToNotify,
|
||||
/* drmSessionManager= */ null,
|
||||
/* playClearSamplesWithoutKeys= */ false,
|
||||
/* disableLoopFilter= */ false,
|
||||
/* useSurfaceYuvOutput= */ false);
|
||||
/* disableLoopFilter= */ 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
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
* @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}
|
||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
|
||||
* @param useSurfaceYuvOutput Directly output YUV to the Surface via ANativeWindow.
|
||||
*/
|
||||
public LibvpxVideoRenderer(
|
||||
boolean scaleToFit,
|
||||
long allowedJoiningTimeMs,
|
||||
Handler eventHandler,
|
||||
VideoRendererEventListener eventListener,
|
||||
int maxDroppedFramesToNotify,
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
boolean disableLoopFilter,
|
||||
boolean useSurfaceYuvOutput) {
|
||||
boolean disableLoopFilter) {
|
||||
super(C.TRACK_TYPE_VIDEO);
|
||||
this.scaleToFit = scaleToFit;
|
||||
this.disableLoopFilter = disableLoopFilter;
|
||||
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
|
||||
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||
this.useSurfaceYuvOutput = useSurfaceYuvOutput;
|
||||
joiningDeadlineMs = C.TIME_UNSET;
|
||||
clearReportedVideoSize();
|
||||
formatHolder = new FormatHolder();
|
||||
@ -364,24 +351,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||
clearReportedVideoSize();
|
||||
clearRenderedFirstFrame();
|
||||
try {
|
||||
setSourceDrmSession(null);
|
||||
releaseDecoder();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -433,18 +406,35 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||
/** Releases the decoder. */
|
||||
@CallSuper
|
||||
protected void releaseDecoder() {
|
||||
if (decoder == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputBuffer = null;
|
||||
outputBuffer = null;
|
||||
decoder.release();
|
||||
decoder = null;
|
||||
decoderCounters.decoderReleaseCount++;
|
||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||
decoderReceivedBuffers = false;
|
||||
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(
|
||||
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
||||
}
|
||||
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
|
||||
if (pendingDrmSession == drmSession) {
|
||||
drmSessionManager.releaseSession(pendingDrmSession);
|
||||
DrmSession<ExoMediaCrypto> session =
|
||||
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
|
||||
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 {
|
||||
pendingDrmSession = null;
|
||||
setSourceDrmSession(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingDrmSession != drmSession) {
|
||||
if (sourceDrmSession != decoderDrmSession) {
|
||||
if (decoderReceivedBuffers) {
|
||||
// Signal end of stream and wait for any final output buffers before re-initialization.
|
||||
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
|
||||
@ -579,18 +573,14 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||
*/
|
||||
protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException {
|
||||
int bufferMode = outputBuffer.mode;
|
||||
boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
|
||||
boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null;
|
||||
boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
|
||||
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
|
||||
if (!renderRgb && !renderYuv && !renderSurface) {
|
||||
if (!renderYuv && !renderSurface) {
|
||||
dropOutputBuffer(outputBuffer);
|
||||
} else {
|
||||
maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
|
||||
if (renderRgb) {
|
||||
renderRgbFrame(outputBuffer, scaleToFit);
|
||||
outputBuffer.release();
|
||||
} else if (renderYuv) {
|
||||
if (renderYuv) {
|
||||
outputBufferRenderer.setOutputBuffer(outputBuffer);
|
||||
// The renderer will release the buffer.
|
||||
} else { // renderSurface
|
||||
@ -668,8 +658,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||
this.surface = surface;
|
||||
this.outputBufferRenderer = outputBufferRenderer;
|
||||
if (surface != null) {
|
||||
outputMode =
|
||||
useSurfaceYuvOutput ? VpxDecoder.OUTPUT_MODE_SURFACE_YUV : VpxDecoder.OUTPUT_MODE_RGB;
|
||||
outputMode = VpxDecoder.OUTPUT_MODE_SURFACE_YUV;
|
||||
} else {
|
||||
outputMode =
|
||||
outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE;
|
||||
@ -704,12 +693,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
drmSession = pendingDrmSession;
|
||||
setDecoderDrmSession(sourceDrmSession);
|
||||
|
||||
ExoMediaCrypto mediaCrypto = null;
|
||||
if (drmSession != null) {
|
||||
mediaCrypto = drmSession.getMediaCrypto();
|
||||
if (decoderDrmSession != null) {
|
||||
mediaCrypto = decoderDrmSession.getMediaCrypto();
|
||||
if (mediaCrypto == null) {
|
||||
DrmSessionException drmError = drmSession.getError();
|
||||
DrmSessionException drmError = decoderDrmSession.getError();
|
||||
if (drmError != null) {
|
||||
// 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.
|
||||
@ -731,8 +721,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||
NUM_OUTPUT_BUFFERS,
|
||||
initialInputBufferSize,
|
||||
mediaCrypto,
|
||||
disableLoopFilter,
|
||||
useSurfaceYuvOutput);
|
||||
disableLoopFilter);
|
||||
decoder.setOutputMode(outputMode);
|
||||
TraceUtil.endSection();
|
||||
long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
|
||||
@ -922,33 +911,16 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
||||
}
|
||||
|
||||
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||
if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||
return false;
|
||||
}
|
||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
||||
@DrmSession.State int drmSessionState = decoderDrmSession.getState();
|
||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
||||
throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
|
||||
}
|
||||
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() {
|
||||
joiningDeadlineMs = allowedJoiningTimeMs > 0
|
||||
? (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_YUV = 0;
|
||||
public static final int OUTPUT_MODE_RGB = 1;
|
||||
public static final int OUTPUT_MODE_SURFACE_YUV = 2;
|
||||
public static final int OUTPUT_MODE_SURFACE_YUV = 1;
|
||||
|
||||
private static final int NO_ERROR = 0;
|
||||
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
|
||||
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
|
||||
* @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.
|
||||
*/
|
||||
public VpxDecoder(
|
||||
@ -60,8 +58,7 @@ import java.nio.ByteBuffer;
|
||||
int numOutputBuffers,
|
||||
int initialInputBufferSize,
|
||||
ExoMediaCrypto exoMediaCrypto,
|
||||
boolean disableLoopFilter,
|
||||
boolean enableSurfaceYuvOutputMode)
|
||||
boolean disableLoopFilter)
|
||||
throws VpxDecoderException {
|
||||
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
||||
if (!VpxLibrary.isAvailable()) {
|
||||
@ -71,7 +68,7 @@ import java.nio.ByteBuffer;
|
||||
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
|
||||
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
|
||||
}
|
||||
vpxDecContext = vpxInit(disableLoopFilter, enableSurfaceYuvOutputMode);
|
||||
vpxDecContext = vpxInit(disableLoopFilter);
|
||||
if (vpxDecContext == 0) {
|
||||
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.
|
||||
*
|
||||
* @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE}, {@link #OUTPUT_MODE_RGB}
|
||||
* and {@link #OUTPUT_MODE_YUV}.
|
||||
* @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE} and {@link
|
||||
* #OUTPUT_MODE_YUV}.
|
||||
*/
|
||||
public void setOutputMode(int 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 vpxDecode(long context, ByteBuffer encoded, int length);
|
||||
|
@ -60,36 +60,19 @@ public final class VpxOutputBuffer extends OutputBuffer {
|
||||
* Initializes the buffer.
|
||||
*
|
||||
* @param timeUs The presentation timestamp for the buffer, in microseconds.
|
||||
* @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE},
|
||||
* {@link VpxDecoder#OUTPUT_MODE_RGB} and {@link VpxDecoder#OUTPUT_MODE_YUV}.
|
||||
* @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE} and {@link
|
||||
* VpxDecoder#OUTPUT_MODE_YUV}.
|
||||
*/
|
||||
public void init(long timeUs, int mode) {
|
||||
this.timeUs = timeUs;
|
||||
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.
|
||||
*
|
||||
* @return Whether the buffer was resized successfully.
|
||||
*/
|
||||
public boolean initForYuvFrame(int width, int height, int yStride, int uvStride,
|
||||
int colorspace) {
|
||||
public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.colorspace = colorspace;
|
||||
|
@ -17,12 +17,6 @@
|
||||
WORKING_DIR := $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
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
|
||||
LOCAL_PATH := $(WORKING_DIR)
|
||||
@ -37,7 +31,7 @@ LOCAL_CPP_EXTENSION := .cc
|
||||
LOCAL_SRC_FILES := vpx_jni.cc
|
||||
LOCAL_LDLIBS := -llog -lz -lm -landroid
|
||||
LOCAL_SHARED_LIBRARIES := libvpx
|
||||
LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures
|
||||
LOCAL_STATIC_LIBRARIES := cpufeatures
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
|
||||
$(call import-module,android/cpufeatures)
|
||||
|
@ -30,8 +30,6 @@
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#include "libyuv.h" // NOLINT
|
||||
|
||||
#define VPX_CODEC_DISABLE_COMPAT 1
|
||||
#include "vpx/vpx_decoder.h"
|
||||
#include "vpx/vp8dx.h"
|
||||
@ -61,7 +59,6 @@
|
||||
(JNIEnv* env, jobject thiz, ##__VA_ARGS__)\
|
||||
|
||||
// JNI references for VpxOutputBuffer class.
|
||||
static jmethodID initForRgbFrame;
|
||||
static jmethodID initForYuvFrame;
|
||||
static jfieldID dataField;
|
||||
static jfieldID outputModeField;
|
||||
@ -393,11 +390,7 @@ class JniBufferManager {
|
||||
};
|
||||
|
||||
struct JniCtx {
|
||||
JniCtx(bool enableBufferManager) {
|
||||
if (enableBufferManager) {
|
||||
buffer_manager = new JniBufferManager();
|
||||
}
|
||||
}
|
||||
JniCtx() { buffer_manager = new JniBufferManager(); }
|
||||
|
||||
~JniCtx() {
|
||||
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);
|
||||
}
|
||||
|
||||
DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
|
||||
jboolean enableBufferManager) {
|
||||
JniCtx* context = new JniCtx(enableBufferManager);
|
||||
DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
|
||||
JniCtx* context = new JniCtx();
|
||||
context->decoder = new vpx_codec_ctx_t();
|
||||
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
|
||||
cfg.threads = android_getCpuCount();
|
||||
@ -469,14 +461,12 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (enableBufferManager) {
|
||||
err = vpx_codec_set_frame_buffer_functions(
|
||||
context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,
|
||||
context->buffer_manager);
|
||||
if (err) {
|
||||
LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.",
|
||||
err);
|
||||
}
|
||||
err = vpx_codec_set_frame_buffer_functions(
|
||||
context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,
|
||||
context->buffer_manager);
|
||||
if (err) {
|
||||
LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.",
|
||||
err);
|
||||
}
|
||||
|
||||
// Populate JNI References.
|
||||
@ -484,8 +474,6 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
|
||||
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
|
||||
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
|
||||
"(IIIII)Z");
|
||||
initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame",
|
||||
"(II)Z");
|
||||
dataField = env->GetFieldID(outputBufferClass, "data",
|
||||
"Ljava/nio/ByteBuffer;");
|
||||
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
|
||||
@ -537,28 +525,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
||||
}
|
||||
|
||||
const int kOutputModeYuv = 0;
|
||||
const int kOutputModeRgb = 1;
|
||||
const int kOutputModeSurfaceYuv = 2;
|
||||
const int kOutputModeSurfaceYuv = 1;
|
||||
|
||||
int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
|
||||
if (outputMode == kOutputModeRgb) {
|
||||
// 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) {
|
||||
if (outputMode == kOutputModeYuv) {
|
||||
const int kColorspaceUnknown = 0;
|
||||
const int kColorspaceBT601 = 1;
|
||||
const int kColorspaceBT709 = 2;
|
||||
@ -616,9 +586,6 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
||||
}
|
||||
} else if (outputMode == kOutputModeSurfaceYuv &&
|
||||
img->fmt != VPX_IMG_FMT_I42016) {
|
||||
if (!context->buffer_manager) {
|
||||
return -1; // enableBufferManager was not set in vpxInit.
|
||||
}
|
||||
int id = *(int*)img->fb_priv;
|
||||
context->buffer_manager->add_ref(id);
|
||||
JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id);
|
||||
|
@ -3,7 +3,7 @@
|
||||
# Constructors accessed via reflection in DefaultRenderersFactory
|
||||
-dontnote 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
|
||||
-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);
|
||||
}
|
||||
|
||||
# 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
|
||||
-dontwarn org.checkerframework.**
|
||||
|
@ -37,7 +37,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
||||
private SampleStream stream;
|
||||
private Format[] streamFormats;
|
||||
private long streamOffsetUs;
|
||||
private boolean readEndOfStream;
|
||||
private long readingPositionUs;
|
||||
private boolean streamIsFinal;
|
||||
|
||||
/**
|
||||
@ -46,7 +46,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
||||
*/
|
||||
public BaseRenderer(int trackType) {
|
||||
this.trackType = trackType;
|
||||
readEndOfStream = true;
|
||||
readingPositionUs = C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -98,7 +98,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
||||
throws ExoPlaybackException {
|
||||
Assertions.checkState(!streamIsFinal);
|
||||
this.stream = stream;
|
||||
readEndOfStream = false;
|
||||
readingPositionUs = offsetUs;
|
||||
streamFormats = formats;
|
||||
streamOffsetUs = offsetUs;
|
||||
onStreamChanged(formats, offsetUs);
|
||||
@ -111,7 +111,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
||||
|
||||
@Override
|
||||
public final boolean hasReadStreamToEnd() {
|
||||
return readEndOfStream;
|
||||
return readingPositionUs == C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long getReadingPositionUs() {
|
||||
return readingPositionUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -132,7 +137,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
||||
@Override
|
||||
public final void resetPosition(long positionUs) throws ExoPlaybackException {
|
||||
streamIsFinal = false;
|
||||
readEndOfStream = false;
|
||||
readingPositionUs = positionUs;
|
||||
onPositionReset(positionUs, false);
|
||||
}
|
||||
|
||||
@ -303,10 +308,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
||||
int result = stream.readData(formatHolder, buffer, formatRequired);
|
||||
if (result == C.RESULT_BUFFER_READ) {
|
||||
if (buffer.isEndOfStream()) {
|
||||
readEndOfStream = true;
|
||||
readingPositionUs = C.TIME_END_OF_SOURCE;
|
||||
return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;
|
||||
}
|
||||
buffer.timeUs += streamOffsetUs;
|
||||
readingPositionUs = Math.max(readingPositionUs, buffer.timeUs);
|
||||
} else if (result == C.RESULT_FORMAT_READ) {
|
||||
Format format = formatHolder.format;
|
||||
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.
|
||||
*/
|
||||
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
|
||||
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_ENCRYPTED} and
|
||||
* {@link #BUFFER_FLAG_DECODE_ONLY}.
|
||||
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE},
|
||||
* {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@ -470,6 +470,7 @@ public final class C {
|
||||
value = {
|
||||
BUFFER_FLAG_KEY_FRAME,
|
||||
BUFFER_FLAG_END_OF_STREAM,
|
||||
BUFFER_FLAG_LAST_SAMPLE,
|
||||
BUFFER_FLAG_ENCRYPTED,
|
||||
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.
|
||||
*/
|
||||
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. */
|
||||
public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
|
||||
/** 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
|
||||
|
||||
/**
|
||||
* Represents an undetermined language as an ISO 639 alpha-3 language code.
|
||||
*/
|
||||
/** Represents an undetermined language as an ISO 639-2 language code. */
|
||||
public static final String LANGUAGE_UNDETERMINED = "und";
|
||||
|
||||
/**
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
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;
|
||||
|
||||
private final Context context;
|
||||
private final @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
|
||||
private final @ExtensionRendererMode int extensionRendererMode;
|
||||
private final long allowedVideoJoiningTimeMs;
|
||||
@Nullable private DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
|
||||
@ExtensionRendererMode private int extensionRendererMode;
|
||||
private long allowedVideoJoiningTimeMs;
|
||||
private boolean playClearSamplesWithoutKeys;
|
||||
private MediaCodecSelector mediaCodecSelector;
|
||||
|
||||
/**
|
||||
* @param context A {@link Context}.
|
||||
*/
|
||||
/** @param context A {@link 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}.
|
||||
* @param extensionRendererMode 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.
|
||||
* @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link
|
||||
* #setExtensionRendererMode(int)}.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
public DefaultRenderersFactory(
|
||||
Context context, @ExtensionRendererMode int extensionRendererMode) {
|
||||
this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #DefaultRenderersFactory(Context, int)} and pass {@link
|
||||
* DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
|
||||
* @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link
|
||||
* #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link
|
||||
* SimpleExoPlayer} or {@link ExoPlayerFactory}.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
@ -132,26 +137,22 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context A {@link Context}.
|
||||
* @param extensionRendererMode 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.
|
||||
* @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
|
||||
* seamlessly join an ongoing playback.
|
||||
* @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link
|
||||
* #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
public DefaultRenderersFactory(
|
||||
Context context,
|
||||
@ExtensionRendererMode int extensionRendererMode,
|
||||
long allowedVideoJoiningTimeMs) {
|
||||
this.context = context;
|
||||
this.extensionRendererMode = extensionRendererMode;
|
||||
this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
|
||||
this.drmSessionManager = null;
|
||||
this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #DefaultRenderersFactory(Context, int, long)} and pass {@link
|
||||
* DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
|
||||
* @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link
|
||||
* #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}, and pass
|
||||
* {@link DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
|
||||
*/
|
||||
@Deprecated
|
||||
public DefaultRenderersFactory(
|
||||
@ -163,6 +164,70 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
this.extensionRendererMode = extensionRendererMode;
|
||||
this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
|
||||
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
|
||||
@ -177,10 +242,26 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
drmSessionManager = this.drmSessionManager;
|
||||
}
|
||||
ArrayList<Renderer> renderersList = new ArrayList<>();
|
||||
buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs,
|
||||
eventHandler, videoRendererEventListener, extensionRendererMode, renderersList);
|
||||
buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(),
|
||||
eventHandler, audioRendererEventListener, extensionRendererMode, renderersList);
|
||||
buildVideoRenderers(
|
||||
context,
|
||||
extensionRendererMode,
|
||||
mediaCodecSelector,
|
||||
drmSessionManager,
|
||||
playClearSamplesWithoutKeys,
|
||||
eventHandler,
|
||||
videoRendererEventListener,
|
||||
allowedVideoJoiningTimeMs,
|
||||
renderersList);
|
||||
buildAudioRenderers(
|
||||
context,
|
||||
extensionRendererMode,
|
||||
mediaCodecSelector,
|
||||
drmSessionManager,
|
||||
playClearSamplesWithoutKeys,
|
||||
buildAudioProcessors(),
|
||||
eventHandler,
|
||||
audioRendererEventListener,
|
||||
renderersList);
|
||||
buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(),
|
||||
extensionRendererMode, renderersList);
|
||||
buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(),
|
||||
@ -194,27 +275,36 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
* Builds video renderers for use by the player.
|
||||
*
|
||||
* @param context The {@link Context} associated with the player.
|
||||
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player
|
||||
* will not be used for DRM protected playbacks.
|
||||
* @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video
|
||||
* renderers can attempt to seamlessly join an ongoing playback.
|
||||
* @param extensionRendererMode The extension renderer mode.
|
||||
* @param mediaCodecSelector A decoder selector.
|
||||
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
|
||||
* 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 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.
|
||||
*/
|
||||
protected void buildVideoRenderers(Context context,
|
||||
protected void buildVideoRenderers(
|
||||
Context context,
|
||||
@ExtensionRendererMode int extensionRendererMode,
|
||||
MediaCodecSelector mediaCodecSelector,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
long allowedVideoJoiningTimeMs, Handler eventHandler,
|
||||
VideoRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
Handler eventHandler,
|
||||
VideoRendererEventListener eventListener,
|
||||
long allowedVideoJoiningTimeMs,
|
||||
ArrayList<Renderer> out) {
|
||||
out.add(
|
||||
new MediaCodecVideoRenderer(
|
||||
context,
|
||||
MediaCodecSelector.DEFAULT,
|
||||
mediaCodecSelector,
|
||||
allowedVideoJoiningTimeMs,
|
||||
drmSessionManager,
|
||||
/* playClearSamplesWithoutKeys= */ false,
|
||||
playClearSamplesWithoutKeys,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
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");
|
||||
Constructor<?> constructor =
|
||||
clazz.getConstructor(
|
||||
boolean.class,
|
||||
long.class,
|
||||
android.os.Handler.class,
|
||||
com.google.android.exoplayer2.video.VideoRendererEventListener.class,
|
||||
@ -242,7 +331,6 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
Renderer renderer =
|
||||
(Renderer)
|
||||
constructor.newInstance(
|
||||
true,
|
||||
allowedVideoJoiningTimeMs,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
@ -261,26 +349,35 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
* Builds audio renderers for use by the player.
|
||||
*
|
||||
* @param context The {@link Context} associated with the player.
|
||||
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player
|
||||
* will not be used for DRM protected playbacks.
|
||||
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio
|
||||
* buffers before output. May be empty.
|
||||
* @param extensionRendererMode The extension renderer mode.
|
||||
* @param mediaCodecSelector A decoder selector.
|
||||
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
|
||||
* 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 eventListener An event listener.
|
||||
* @param extensionRendererMode The extension renderer mode.
|
||||
* @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,
|
||||
AudioProcessor[] audioProcessors, Handler eventHandler,
|
||||
AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
AudioProcessor[] audioProcessors,
|
||||
Handler eventHandler,
|
||||
AudioRendererEventListener eventListener,
|
||||
ArrayList<Renderer> out) {
|
||||
out.add(
|
||||
new MediaCodecAudioRenderer(
|
||||
context,
|
||||
MediaCodecSelector.DEFAULT,
|
||||
mediaCodecSelector,
|
||||
drmSessionManager,
|
||||
/* playClearSamplesWithoutKeys= */ false,
|
||||
playClearSamplesWithoutKeys,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
AudioCapabilities.getCapabilities(context),
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.support.annotation.IntDef;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.IOException;
|
||||
@ -34,7 +35,7 @@ public final class ExoPlaybackException extends Exception {
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED})
|
||||
@IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED, TYPE_REMOTE})
|
||||
public @interface Type {}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
@ -66,7 +73,7 @@ public final class ExoPlaybackException extends Exception {
|
||||
*/
|
||||
public final int rendererIndex;
|
||||
|
||||
private final Throwable cause;
|
||||
@Nullable private final Throwable cause;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
super(cause);
|
||||
this.type = type;
|
||||
@ -106,6 +123,13 @@ public final class ExoPlaybackException extends Exception {
|
||||
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}.
|
||||
*
|
||||
@ -113,7 +137,7 @@ public final class ExoPlaybackException extends Exception {
|
||||
*/
|
||||
public IOException getSourceException() {
|
||||
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() {
|
||||
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() {
|
||||
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.source.ClippingMediaSource;
|
||||
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.MediaSource;
|
||||
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.text.TextRenderer;
|
||||
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
|
||||
* 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
|
||||
* implementations for regular media files ({@link ExtractorMediaSource}), DASH
|
||||
* implementations for progressive media files ({@link ProgressiveMediaSource}), DASH
|
||||
* (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an
|
||||
* implementation for loading single media samples ({@link SingleSampleMediaSource}) that's
|
||||
* most often used for side-loaded subtitle files, and implementations for building more
|
||||
|
@ -58,7 +58,8 @@ public final class ExoPlayerFactory {
|
||||
LoadControl loadControl,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
|
||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode);
|
||||
RenderersFactory renderersFactory =
|
||||
new DefaultRenderersFactory(context).setExtensionRendererMode(extensionRendererMode);
|
||||
return newSimpleInstance(
|
||||
context, renderersFactory, trackSelector, loadControl, drmSessionManager);
|
||||
}
|
||||
@ -88,7 +89,9 @@ public final class ExoPlayerFactory {
|
||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
|
||||
long allowedVideoJoiningTimeMs) {
|
||||
RenderersFactory renderersFactory =
|
||||
new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs);
|
||||
new DefaultRenderersFactory(context)
|
||||
.setExtensionRendererMode(extensionRendererMode)
|
||||
.setAllowedVideoJoiningTimeMs(allowedVideoJoiningTimeMs);
|
||||
return newSimpleInstance(
|
||||
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);
|
||||
}
|
||||
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() {
|
||||
setState(Player.STATE_ENDED);
|
||||
// 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". */
|
||||
// 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}. */
|
||||
// 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.
|
||||
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
|
||||
* integer version 123045006 (123-045-006).
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final int VERSION_INT = 2009003;
|
||||
public static final int VERSION_INT = 2009005;
|
||||
|
||||
/**
|
||||
* 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
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -932,7 +932,7 @@ public final class Format implements Parcelable {
|
||||
this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay;
|
||||
this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding;
|
||||
this.selectionFlags = selectionFlags;
|
||||
this.language = language;
|
||||
this.language = Util.normalizeLanguageCode(language);
|
||||
this.accessibilityChannel = accessibilityChannel;
|
||||
this.subsampleOffsetUs = subsampleOffsetUs;
|
||||
this.initializationData =
|
||||
|
@ -89,7 +89,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
this.info = info;
|
||||
sampleStreams = new SampleStream[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() {
|
||||
disableTrackSelectionsInResult();
|
||||
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}. */
|
||||
private static MediaPeriod createMediaPeriod(
|
||||
MediaPeriodId id, MediaSource mediaSource, Allocator allocator) {
|
||||
MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator);
|
||||
if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) {
|
||||
MediaPeriodId id,
|
||||
MediaSource mediaSource,
|
||||
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 =
|
||||
new ClippingMediaPeriod(
|
||||
mediaPeriod,
|
||||
/* enableInitialDiscontinuity= */ true,
|
||||
/* startUs= */ 0,
|
||||
id.endPositionUs);
|
||||
mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, endPositionUs);
|
||||
}
|
||||
return mediaPeriod;
|
||||
}
|
||||
|
||||
/** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */
|
||||
private static void releaseMediaPeriod(
|
||||
MediaPeriodId id, MediaSource mediaSource, MediaPeriod mediaPeriod) {
|
||||
long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) {
|
||||
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);
|
||||
} else {
|
||||
mediaSource.releasePeriod(mediaPeriod);
|
||||
|
@ -33,7 +33,14 @@ import com.google.android.exoplayer2.util.Util;
|
||||
*/
|
||||
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
|
||||
* known.
|
||||
*/
|
||||
@ -53,26 +60,51 @@ import com.google.android.exoplayer2.util.Util;
|
||||
MediaPeriodId id,
|
||||
long startPositionUs,
|
||||
long contentPositionUs,
|
||||
long endPositionUs,
|
||||
long durationUs,
|
||||
boolean isLastInTimelinePeriod,
|
||||
boolean isFinal) {
|
||||
this.id = id;
|
||||
this.startPositionUs = startPositionUs;
|
||||
this.contentPositionUs = contentPositionUs;
|
||||
this.endPositionUs = endPositionUs;
|
||||
this.durationUs = durationUs;
|
||||
this.isLastInTimelinePeriod = isLastInTimelinePeriod;
|
||||
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) {
|
||||
return new MediaPeriodInfo(
|
||||
id,
|
||||
startPositionUs,
|
||||
contentPositionUs,
|
||||
durationUs,
|
||||
isLastInTimelinePeriod,
|
||||
isFinal);
|
||||
return startPositionUs == this.startPositionUs
|
||||
? this
|
||||
: new MediaPeriodInfo(
|
||||
id,
|
||||
startPositionUs,
|
||||
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,
|
||||
isLastInTimelinePeriod,
|
||||
isFinal);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -86,6 +118,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
MediaPeriodInfo that = (MediaPeriodInfo) o;
|
||||
return startPositionUs == that.startPositionUs
|
||||
&& contentPositionUs == that.contentPositionUs
|
||||
&& endPositionUs == that.endPositionUs
|
||||
&& durationUs == that.durationUs
|
||||
&& isLastInTimelinePeriod == that.isLastInTimelinePeriod
|
||||
&& isFinal == that.isFinal
|
||||
@ -98,6 +131,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
result = 31 * result + id.hashCode();
|
||||
result = 31 * result + (int) startPositionUs;
|
||||
result = 31 * result + (int) contentPositionUs;
|
||||
result = 31 * result + (int) endPositionUs;
|
||||
result = 31 * result + (int) durationUs;
|
||||
result = 31 * result + (isLastInTimelinePeriod ? 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
|
||||
* queued media periods to take into account the new timeline.
|
||||
* Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long, long)} to update the queued
|
||||
* media periods to take into account the new timeline.
|
||||
*/
|
||||
public void setTimeline(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
|
||||
* consistent with the new timeline.
|
||||
*
|
||||
* @param playingPeriodId The current playing media period identifier.
|
||||
* @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.
|
||||
*/
|
||||
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
|
||||
// is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be
|
||||
// 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 periodHolder = getFrontPeriod();
|
||||
while (periodHolder != null) {
|
||||
MediaPeriodInfo oldPeriodInfo = periodHolder.info;
|
||||
|
||||
// Get period info based on new timeline.
|
||||
MediaPeriodInfo newPeriodInfo;
|
||||
if (previousPeriodHolder == null) {
|
||||
long previousDurationUs = periodHolder.info.durationUs;
|
||||
periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info);
|
||||
if (!canKeepAfterMediaPeriodHolder(periodHolder, previousDurationUs)) {
|
||||
return !removeAfter(periodHolder);
|
||||
}
|
||||
// The id and start position of the first period have already been verified by
|
||||
// ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline
|
||||
// and isLastInPeriod flags.
|
||||
newPeriodInfo = getUpdatedMediaPeriodInfo(oldPeriodInfo);
|
||||
} else {
|
||||
// Check this period holder still follows the previous one, based on the new timeline.
|
||||
if (periodIndex == C.INDEX_UNSET
|
||||
|| !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) {
|
||||
newPeriodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);
|
||||
if (newPeriodInfo == null) {
|
||||
// We've loaded a next media period that is not in the new timeline.
|
||||
return !removeAfter(previousPeriodHolder);
|
||||
}
|
||||
// Update the period holder.
|
||||
periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info);
|
||||
// Check the media period information matches the new timeline.
|
||||
if (!canKeepMediaPeriodHolder(periodHolder, periodInfo)) {
|
||||
if (!canKeepMediaPeriodHolder(oldPeriodInfo, newPeriodInfo)) {
|
||||
// The new media period has a different id or start position.
|
||||
return !removeAfter(previousPeriodHolder);
|
||||
} else if (!canKeepAfterMediaPeriodHolder(periodHolder, periodInfo.durationUs)) {
|
||||
return !removeAfter(periodHolder);
|
||||
}
|
||||
}
|
||||
|
||||
if (periodHolder.info.isLastInTimelinePeriod) {
|
||||
// Move on to the next timeline period index, if there is one.
|
||||
periodIndex =
|
||||
timeline.getNextPeriodIndex(
|
||||
periodIndex, period, window, repeatMode, shuffleModeEnabled);
|
||||
// Use new period info, but keep old content position.
|
||||
periodHolder.info = newPeriodInfo.copyWithContentPositionUs(oldPeriodInfo.contentPositionUs);
|
||||
|
||||
if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) {
|
||||
// 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;
|
||||
@ -364,13 +366,14 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
long durationUs =
|
||||
id.isAd()
|
||||
? 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()
|
||||
: id.endPositionUs);
|
||||
: info.endPositionUs);
|
||||
return new MediaPeriodInfo(
|
||||
id,
|
||||
info.startPositionUs,
|
||||
info.contentPositionUs,
|
||||
info.endPositionUs,
|
||||
durationUs,
|
||||
isLastInPeriod,
|
||||
isLastInTimeline);
|
||||
@ -409,11 +412,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
|
||||
long endPositionUs =
|
||||
nextAdGroupIndex == C.INDEX_UNSET
|
||||
? C.TIME_UNSET
|
||||
: period.getAdGroupTimeUs(nextAdGroupIndex);
|
||||
return new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs);
|
||||
return new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);
|
||||
} else {
|
||||
int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex);
|
||||
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
|
||||
* {@code info}.
|
||||
* Returns whether a period described by {@code oldInfo} can be kept for playing the media period
|
||||
* described by {@code newInfo}.
|
||||
*/
|
||||
private boolean canKeepMediaPeriodHolder(MediaPeriodHolder periodHolder, MediaPeriodInfo info) {
|
||||
MediaPeriodInfo periodHolderInfo = periodHolder.info;
|
||||
return periodHolderInfo.startPositionUs == info.startPositionUs
|
||||
&& periodHolderInfo.id.equals(info.id);
|
||||
private boolean canKeepMediaPeriodHolder(MediaPeriodInfo oldInfo, MediaPeriodInfo newInfo) {
|
||||
return oldInfo.startPositionUs == newInfo.startPositionUs && oldInfo.id.equals(newInfo.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether periods after {@code periodHolder} can be kept for playing given its previous
|
||||
* duration.
|
||||
* Returns whether a duration change of a period is compatible with keeping the following periods.
|
||||
*/
|
||||
private boolean canKeepAfterMediaPeriodHolder(
|
||||
MediaPeriodHolder periodHolder, long previousDurationUs) {
|
||||
return previousDurationUs == C.TIME_UNSET || previousDurationUs == periodHolder.info.durationUs;
|
||||
private boolean areDurationsCompatible(long previousDurationUs, long newDurationUs) {
|
||||
return previousDurationUs == C.TIME_UNSET || previousDurationUs == newDurationUs;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -645,7 +640,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
}
|
||||
} else {
|
||||
// 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) {
|
||||
// The next ad group can't be played. Play content from the previous end position instead.
|
||||
return getMediaPeriodInfoForContent(
|
||||
@ -703,6 +698,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
id,
|
||||
startPositionUs,
|
||||
contentPositionUs,
|
||||
/* endPositionUs= */ C.TIME_UNSET,
|
||||
durationUs,
|
||||
/* isLastInTimelinePeriod= */ false,
|
||||
/* isFinal= */ false);
|
||||
@ -711,13 +707,13 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
private MediaPeriodInfo getMediaPeriodInfoForContent(
|
||||
Object periodUid, long startPositionUs, long windowSequenceNumber) {
|
||||
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
|
||||
MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);
|
||||
boolean isLastInPeriod = isLastInPeriod(id);
|
||||
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
|
||||
long endPositionUs =
|
||||
nextAdGroupIndex != C.INDEX_UNSET
|
||||
? period.getAdGroupTimeUs(nextAdGroupIndex)
|
||||
: C.TIME_UNSET;
|
||||
MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs);
|
||||
boolean isLastInPeriod = isLastInPeriod(id);
|
||||
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
|
||||
long durationUs =
|
||||
endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE
|
||||
? period.durationUs
|
||||
@ -726,13 +722,14 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
id,
|
||||
startPositionUs,
|
||||
/* contentPositionUs= */ C.TIME_UNSET,
|
||||
endPositionUs,
|
||||
durationUs,
|
||||
isLastInPeriod,
|
||||
isLastInTimeline);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -122,6 +122,11 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getReadingPositionUs() {
|
||||
return C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setCurrentStreamFinal() {
|
||||
streamIsFinal = true;
|
||||
|
@ -160,6 +160,16 @@ public interface Renderer extends PlayerMessage.Target {
|
||||
*/
|
||||
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
|
||||
* 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
|
||||
* be obtained from {@link ExoPlayerFactory}.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public class SimpleExoPlayer extends BasePlayer
|
||||
implements ExoPlayer,
|
||||
Player.AudioComponent,
|
||||
@ -94,25 +93,25 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
|
||||
private final AudioFocusManager audioFocusManager;
|
||||
|
||||
private Format videoFormat;
|
||||
private Format audioFormat;
|
||||
@Nullable private Format videoFormat;
|
||||
@Nullable private Format audioFormat;
|
||||
|
||||
private Surface surface;
|
||||
@Nullable private Surface surface;
|
||||
private boolean ownsSurface;
|
||||
private @C.VideoScalingMode int videoScalingMode;
|
||||
private SurfaceHolder surfaceHolder;
|
||||
private TextureView textureView;
|
||||
@Nullable private SurfaceHolder surfaceHolder;
|
||||
@Nullable private TextureView textureView;
|
||||
private int surfaceWidth;
|
||||
private int surfaceHeight;
|
||||
private DecoderCounters videoDecoderCounters;
|
||||
private DecoderCounters audioDecoderCounters;
|
||||
@Nullable private DecoderCounters videoDecoderCounters;
|
||||
@Nullable private DecoderCounters audioDecoderCounters;
|
||||
private int audioSessionId;
|
||||
private AudioAttributes audioAttributes;
|
||||
private float audioVolume;
|
||||
private MediaSource mediaSource;
|
||||
@Nullable private MediaSource mediaSource;
|
||||
private List<Cue> currentCues;
|
||||
private VideoFrameMetadataListener videoFrameMetadataListener;
|
||||
private CameraMotionListener cameraMotionListener;
|
||||
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
|
||||
@Nullable private CameraMotionListener cameraMotionListener;
|
||||
private boolean hasNotifiedFullWrongThreadWarning;
|
||||
|
||||
/**
|
||||
@ -558,30 +557,26 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
return audioDecoderCounters;
|
||||
}
|
||||
@ -1053,7 +1048,8 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getCurrentManifest() {
|
||||
@Nullable
|
||||
public Object getCurrentManifest() {
|
||||
verifyApplicationThread();
|
||||
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
|
||||
* yet.
|
||||
* yet or the current player is idle.
|
||||
*
|
||||
* @param player The {@link Player} for which data will be collected.
|
||||
*/
|
||||
public void setPlayer(Player player) {
|
||||
Assertions.checkState(this.player == null);
|
||||
Assertions.checkState(
|
||||
this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty());
|
||||
this.player = Assertions.checkNotNull(player);
|
||||
}
|
||||
|
||||
@ -488,7 +489,10 @@ public class AnalyticsCollector
|
||||
|
||||
@Override
|
||||
public final void onPlayerError(ExoPlaybackException error) {
|
||||
EventTime eventTime = generatePlayingMediaPeriodEventTime();
|
||||
EventTime eventTime =
|
||||
error.type == ExoPlaybackException.TYPE_SOURCE
|
||||
? generateLoadingMediaPeriodEventTime()
|
||||
: generatePlayingMediaPeriodEventTime();
|
||||
for (AnalyticsListener listener : listeners) {
|
||||
listener.onPlayerError(eventTime, error);
|
||||
}
|
||||
|
@ -147,6 +147,7 @@ public interface AudioRendererEventListener {
|
||||
* Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
|
||||
*/
|
||||
public void disabled(final DecoderCounters counters) {
|
||||
counters.ensureUpdated();
|
||||
if (listener != null) {
|
||||
handler.post(
|
||||
() -> {
|
||||
|
@ -418,7 +418,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
isInputPcm = Util.isEncodingLinearPcm(inputEncoding);
|
||||
shouldConvertHighResIntPcmToFloat =
|
||||
enableConvertHighResIntPcmToFloat
|
||||
&& supportsOutput(channelCount, C.ENCODING_PCM_32BIT)
|
||||
&& supportsOutput(channelCount, C.ENCODING_PCM_FLOAT)
|
||||
&& Util.isEncodingHighResolutionIntegerPcm(inputEncoding);
|
||||
if (isInputPcm) {
|
||||
pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount);
|
||||
|
@ -16,7 +16,6 @@
|
||||
package com.google.android.exoplayer2.audio;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCrypto;
|
||||
@ -66,7 +65,6 @@ import java.util.List;
|
||||
* underlying audio track.
|
||||
* </ul>
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
|
||||
|
||||
/**
|
||||
@ -548,7 +546,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
try {
|
||||
super.onDisabled();
|
||||
} finally {
|
||||
decoderCounters.ensureUpdated();
|
||||
eventDispatcher.disabled(decoderCounters);
|
||||
}
|
||||
}
|
||||
|
@ -106,8 +106,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
? extends AudioDecoderException> decoder;
|
||||
private DecoderInputBuffer inputBuffer;
|
||||
private SimpleOutputBuffer outputBuffer;
|
||||
private DrmSession<ExoMediaCrypto> drmSession;
|
||||
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
||||
@Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
|
||||
@Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;
|
||||
|
||||
@ReinitializationState private int decoderReinitializationState;
|
||||
private boolean decoderReceivedBuffers;
|
||||
@ -462,12 +462,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
}
|
||||
|
||||
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||
if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||
return false;
|
||||
}
|
||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
||||
@DrmSession.State int drmSessionState = decoderDrmSession.getState();
|
||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
||||
throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
|
||||
}
|
||||
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
||||
}
|
||||
@ -568,25 +568,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
audioTrackNeedsConfigure = true;
|
||||
waitingForKeys = false;
|
||||
try {
|
||||
setSourceDrmSession(null);
|
||||
releaseDecoder();
|
||||
audioSink.reset();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -615,12 +601,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
return;
|
||||
}
|
||||
|
||||
drmSession = pendingDrmSession;
|
||||
setDecoderDrmSession(sourceDrmSession);
|
||||
|
||||
ExoMediaCrypto mediaCrypto = null;
|
||||
if (drmSession != null) {
|
||||
mediaCrypto = drmSession.getMediaCrypto();
|
||||
if (decoderDrmSession != null) {
|
||||
mediaCrypto = decoderDrmSession.getMediaCrypto();
|
||||
if (mediaCrypto == null) {
|
||||
DrmSessionException drmError = drmSession.getError();
|
||||
DrmSessionException drmError = decoderDrmSession.getError();
|
||||
if (drmError != null) {
|
||||
// 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.
|
||||
@ -646,17 +633,34 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
}
|
||||
|
||||
private void releaseDecoder() {
|
||||
if (decoder == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputBuffer = null;
|
||||
outputBuffer = null;
|
||||
decoder.release();
|
||||
decoder = null;
|
||||
decoderCounters.decoderReleaseCount++;
|
||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||
decoderReceivedBuffers = false;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
|
||||
@ -671,13 +675,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
throw ExoPlaybackException.createForRenderer(
|
||||
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
||||
}
|
||||
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(),
|
||||
inputFormat.drmInitData);
|
||||
if (pendingDrmSession == drmSession) {
|
||||
drmSessionManager.releaseSession(pendingDrmSession);
|
||||
DrmSession<ExoMediaCrypto> session =
|
||||
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
|
||||
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 {
|
||||
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;
|
||||
|
||||
public CryptoInfo() {
|
||||
frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null;
|
||||
frameworkCryptoInfo = new android.media.MediaCodec.CryptoInfo();
|
||||
patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null;
|
||||
}
|
||||
|
||||
@ -79,34 +79,8 @@ public final class CryptoInfo {
|
||||
this.mode = mode;
|
||||
this.encryptedBlocks = encryptedBlocks;
|
||||
this.clearBlocks = clearBlocks;
|
||||
if (Util.SDK_INT >= 16) {
|
||||
updateFrameworkCryptoInfoV16();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
// Update frameworkCryptoInfo fields directly because CryptoInfo.set performs an unnecessary
|
||||
// object allocation on Android N.
|
||||
frameworkCryptoInfo.numSubSamples = numSubSamples;
|
||||
frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData;
|
||||
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)
|
||||
private static final class PatternHolderV24 {
|
||||
|
||||
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaDrm;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.support.annotation.Nullable;
|
||||
@ -27,7 +26,6 @@ import java.util.Map;
|
||||
/**
|
||||
* A DRM session.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public interface DrmSession<T extends ExoMediaCrypto> {
|
||||
|
||||
/**
|
||||
|
@ -15,14 +15,12 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Looper;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
|
||||
/**
|
||||
* Manages a DRM session.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public interface DrmSessionManager<T extends ExoMediaCrypto> {
|
||||
|
||||
/**
|
||||
|
@ -15,14 +15,5 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
/**
|
||||
* An opaque {@link android.media.MediaCrypto} equivalent.
|
||||
*/
|
||||
public interface ExoMediaCrypto {
|
||||
|
||||
/**
|
||||
* @see android.media.MediaCrypto#requiresSecureDecoderComponent(String)
|
||||
*/
|
||||
boolean requiresSecureDecoderComponent(String mimeType);
|
||||
|
||||
}
|
||||
/** An opaque {@link android.media.MediaCrypto} equivalent. */
|
||||
public interface ExoMediaCrypto {}
|
||||
|
@ -265,11 +265,9 @@ public interface ExoMediaDrm<T extends ExoMediaCrypto> {
|
||||
|
||||
/**
|
||||
* @see android.media.MediaCrypto#MediaCrypto(UUID, byte[])
|
||||
*
|
||||
* @param initData Opaque initialization data specific to the crypto scheme.
|
||||
* @param sessionId The DRM session ID.
|
||||
* @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.
|
||||
* @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;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
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 {
|
||||
|
||||
private final MediaCrypto mediaCrypto;
|
||||
private final boolean forceAllowInsecureDecoderComponents;
|
||||
/** The DRM scheme UUID. */
|
||||
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) {
|
||||
this(mediaCrypto, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
public FrameworkMediaCrypto(
|
||||
UUID uuid, byte[] sessionId, boolean forceAllowInsecureDecoderComponents) {
|
||||
this.uuid = uuid;
|
||||
this.sessionId = sessionId;
|
||||
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.TargetApi;
|
||||
import android.media.DeniedByServerException;
|
||||
import android.media.MediaCrypto;
|
||||
import android.media.MediaCryptoException;
|
||||
import android.media.MediaDrm;
|
||||
import android.media.MediaDrmException;
|
||||
@ -210,7 +209,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
|
||||
boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
|
||||
&& C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
|
||||
return new FrameworkMediaCrypto(
|
||||
new MediaCrypto(adjustUuid(uuid), initData), forceAllowInsecureDecoderComponents);
|
||||
adjustUuid(uuid), initData, forceAllowInsecureDecoderComponents);
|
||||
}
|
||||
|
||||
private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) {
|
||||
|
@ -64,6 +64,9 @@ import com.google.android.exoplayer2.util.Util;
|
||||
this.flags = flags;
|
||||
this.durationUs = durationUs;
|
||||
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_DETECT_ACCESS_UNITS,
|
||||
FLAG_IGNORE_SPLICE_INFO_STREAM,
|
||||
FLAG_OVERRIDE_CAPTION_DESCRIPTORS
|
||||
FLAG_OVERRIDE_CAPTION_DESCRIPTORS,
|
||||
FLAG_IGNORE_HDMV_DTS_STREAM
|
||||
})
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
|
||||
@ -142,8 +149,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
||||
case TsExtractor.TS_STREAM_TYPE_AC3:
|
||||
case TsExtractor.TS_STREAM_TYPE_E_AC3:
|
||||
return new PesReader(new Ac3Reader(esInfo.language));
|
||||
case TsExtractor.TS_STREAM_TYPE_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));
|
||||
case TsExtractor.TS_STREAM_TYPE_H262:
|
||||
return new PesReader(new H262Reader(buildUserDataReader(esInfo)));
|
||||
|
@ -100,7 +100,7 @@ public interface TsPayloadReader {
|
||||
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 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;
|
||||
|
||||
/** Information about a {@link MediaCodec} for a given mime type. */
|
||||
@TargetApi(16)
|
||||
@SuppressWarnings("InlinedApi")
|
||||
public final class MediaCodecInfo {
|
||||
|
||||
|
@ -20,6 +20,7 @@ import android.media.MediaCodec;
|
||||
import android.media.MediaCodec.CodecException;
|
||||
import android.media.MediaCodec.CryptoException;
|
||||
import android.media.MediaCrypto;
|
||||
import android.media.MediaCryptoException;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Bundle;
|
||||
import android.os.Looper;
|
||||
@ -57,7 +58,6 @@ import java.util.List;
|
||||
/**
|
||||
* An abstract renderer that uses {@link MediaCodec} to decode samples for rendering.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
|
||||
/**
|
||||
@ -239,14 +239,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
|
||||
@Documented
|
||||
@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 {}
|
||||
/** No special action should be taken. */
|
||||
private static final int DRAIN_ACTION_NONE = 0;
|
||||
/** The codec should be flushed. */
|
||||
private static final int DRAIN_ACTION_FLUSH = 1;
|
||||
/** The codec should be re-initialized. */
|
||||
private static final int DRAIN_ACTION_REINITIALIZE = 2;
|
||||
/** The codec should be flushed and updated to use the pending DRM session. */
|
||||
private static final int DRAIN_ACTION_UPDATE_DRM_SESSION = 2;
|
||||
/** The codec should be reinitialized. */
|
||||
private static final int DRAIN_ACTION_REINITIALIZE = 3;
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@ -287,13 +294,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
private final DecoderInputBuffer flagsOnlyBuffer;
|
||||
private final FormatHolder formatHolder;
|
||||
private final TimedValueQueue<Format> formatQueue;
|
||||
private final List<Long> decodeOnlyPresentationTimestamps;
|
||||
private final ArrayList<Long> decodeOnlyPresentationTimestamps;
|
||||
private final MediaCodec.BufferInfo outputBufferInfo;
|
||||
|
||||
@Nullable private Format inputFormat;
|
||||
private Format outputFormat;
|
||||
private DrmSession<FrameworkMediaCrypto> drmSession;
|
||||
private DrmSession<FrameworkMediaCrypto> pendingDrmSession;
|
||||
@Nullable private DrmSession<FrameworkMediaCrypto> codecDrmSession;
|
||||
@Nullable private DrmSession<FrameworkMediaCrypto> sourceDrmSession;
|
||||
@Nullable private MediaCrypto mediaCrypto;
|
||||
private boolean mediaCryptoRequiresSecureDecoder;
|
||||
private long renderTimeLimitMs;
|
||||
private float rendererOperatingRate;
|
||||
@Nullable private MediaCodec codec;
|
||||
@ -356,7 +365,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
boolean playClearSamplesWithoutKeys,
|
||||
float assumedMinimumCodecOperatingRate) {
|
||||
super(trackType);
|
||||
Assertions.checkState(Util.SDK_INT >= 16);
|
||||
this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector);
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||
@ -457,29 +465,36 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
drmSession = pendingDrmSession;
|
||||
setCodecDrmSession(sourceDrmSession);
|
||||
|
||||
String mimeType = inputFormat.sampleMimeType;
|
||||
MediaCrypto wrappedMediaCrypto = null;
|
||||
boolean drmSessionRequiresSecureDecoder = false;
|
||||
if (drmSession != null) {
|
||||
FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto();
|
||||
if (codecDrmSession != null) {
|
||||
if (mediaCrypto == null) {
|
||||
DrmSessionException drmError = drmSession.getError();
|
||||
if (drmError != null) {
|
||||
// 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.
|
||||
FrameworkMediaCrypto sessionMediaCrypto = codecDrmSession.getMediaCrypto();
|
||||
if (sessionMediaCrypto == null) {
|
||||
DrmSessionException drmError = codecDrmSession.getError();
|
||||
if (drmError != null) {
|
||||
// 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.
|
||||
} else {
|
||||
// The drm session isn't open yet.
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// The drm session isn't open yet.
|
||||
return;
|
||||
try {
|
||||
mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId);
|
||||
} catch (MediaCryptoException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
mediaCryptoRequiresSecureDecoder =
|
||||
!sessionMediaCrypto.forceAllowInsecureDecoderComponents
|
||||
&& mediaCrypto.requiresSecureDecoderComponent(mimeType);
|
||||
}
|
||||
} else {
|
||||
wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
|
||||
drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
|
||||
}
|
||||
if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
|
||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
||||
@DrmSession.State int drmSessionState = codecDrmSession.getState();
|
||||
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) {
|
||||
// Wait for keys.
|
||||
return;
|
||||
@ -488,7 +503,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
}
|
||||
|
||||
try {
|
||||
maybeInitCodecWithFallback(wrappedMediaCrypto, drmSessionRequiresSecureDecoder);
|
||||
maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder);
|
||||
} catch (DecoderInitializationException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
@ -537,7 +552,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
||||
inputStreamEnded = false;
|
||||
outputStreamEnded = false;
|
||||
flushOrReinitCodec();
|
||||
flushOrReinitializeCodec();
|
||||
formatQueue.clear();
|
||||
}
|
||||
|
||||
@ -552,7 +567,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
inputFormat = null;
|
||||
if (drmSession != null || pendingDrmSession != null) {
|
||||
if (sourceDrmSession != null || codecDrmSession != null) {
|
||||
// TODO: Do something better with this case.
|
||||
onReset();
|
||||
} else {
|
||||
@ -565,51 +580,40 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
try {
|
||||
releaseCodec();
|
||||
} finally {
|
||||
try {
|
||||
if (drmSession != null) {
|
||||
drmSessionManager.releaseSession(drmSession);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
|
||||
drmSessionManager.releaseSession(pendingDrmSession);
|
||||
}
|
||||
} finally {
|
||||
drmSession = null;
|
||||
pendingDrmSession = null;
|
||||
}
|
||||
}
|
||||
setSourceDrmSession(null);
|
||||
}
|
||||
}
|
||||
|
||||
protected void releaseCodec() {
|
||||
availableCodecInfos = null;
|
||||
if (codec != null) {
|
||||
codecInfo = null;
|
||||
codecFormat = null;
|
||||
resetInputBuffer();
|
||||
resetOutputBuffer();
|
||||
resetCodecBuffers();
|
||||
waitingForKeys = false;
|
||||
codecHotswapDeadlineMs = C.TIME_UNSET;
|
||||
decodeOnlyPresentationTimestamps.clear();
|
||||
decoderCounters.decoderReleaseCount++;
|
||||
try {
|
||||
codec.stop();
|
||||
} finally {
|
||||
codecInfo = null;
|
||||
codecFormat = null;
|
||||
resetInputBuffer();
|
||||
resetOutputBuffer();
|
||||
resetCodecBuffers();
|
||||
waitingForKeys = false;
|
||||
codecHotswapDeadlineMs = C.TIME_UNSET;
|
||||
decodeOnlyPresentationTimestamps.clear();
|
||||
try {
|
||||
if (codec != null) {
|
||||
decoderCounters.decoderReleaseCount++;
|
||||
try {
|
||||
codec.release();
|
||||
codec.stop();
|
||||
} finally {
|
||||
codec = null;
|
||||
if (drmSession != null && pendingDrmSession != drmSession) {
|
||||
try {
|
||||
drmSessionManager.releaseSession(drmSession);
|
||||
} finally {
|
||||
drmSession = null;
|
||||
}
|
||||
}
|
||||
codec.release();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
codec = null;
|
||||
try {
|
||||
if (mediaCrypto != null) {
|
||||
mediaCrypto.release();
|
||||
}
|
||||
} finally {
|
||||
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
|
||||
* #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.
|
||||
*/
|
||||
protected final void flushOrReinitCodec() throws ExoPlaybackException {
|
||||
if (flushOrReleaseCodec()) {
|
||||
protected final boolean flushOrReinitializeCodec() throws ExoPlaybackException {
|
||||
boolean released = flushOrReleaseCodec();
|
||||
if (released) {
|
||||
maybeInitCodec();
|
||||
}
|
||||
return released;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -729,18 +736,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
}
|
||||
|
||||
private void maybeInitCodecWithFallback(
|
||||
MediaCrypto crypto, boolean drmSessionRequiresSecureDecoder)
|
||||
MediaCrypto crypto, boolean mediaCryptoRequiresSecureDecoder)
|
||||
throws DecoderInitializationException {
|
||||
if (availableCodecInfos == null) {
|
||||
try {
|
||||
availableCodecInfos =
|
||||
new ArrayDeque<>(getAvailableCodecInfos(drmSessionRequiresSecureDecoder));
|
||||
new ArrayDeque<>(getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder));
|
||||
preferredDecoderInitializationException = null;
|
||||
} catch (DecoderQueryException e) {
|
||||
throw new DecoderInitializationException(
|
||||
inputFormat,
|
||||
e,
|
||||
drmSessionRequiresSecureDecoder,
|
||||
mediaCryptoRequiresSecureDecoder,
|
||||
DecoderInitializationException.DECODER_QUERY_ERROR);
|
||||
}
|
||||
}
|
||||
@ -749,7 +756,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
throw new DecoderInitializationException(
|
||||
inputFormat,
|
||||
/* cause= */ null,
|
||||
drmSessionRequiresSecureDecoder,
|
||||
mediaCryptoRequiresSecureDecoder,
|
||||
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);
|
||||
}
|
||||
|
||||
@ -768,7 +775,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
availableCodecInfos.removeFirst();
|
||||
DecoderInitializationException exception =
|
||||
new DecoderInitializationException(
|
||||
inputFormat, e, drmSessionRequiresSecureDecoder, codecInfo.name);
|
||||
inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo.name);
|
||||
if (preferredDecoderInitializationException == null) {
|
||||
preferredDecoderInitializationException = exception;
|
||||
} else {
|
||||
@ -784,11 +791,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
availableCodecInfos = null;
|
||||
}
|
||||
|
||||
private List<MediaCodecInfo> getAvailableCodecInfos(boolean drmSessionRequiresSecureDecoder)
|
||||
private List<MediaCodecInfo> getAvailableCodecInfos(boolean mediaCryptoRequiresSecureDecoder)
|
||||
throws DecoderQueryException {
|
||||
List<MediaCodecInfo> codecInfos =
|
||||
getDecoderInfos(mediaCodecSelector, inputFormat, drmSessionRequiresSecureDecoder);
|
||||
if (codecInfos.isEmpty() && drmSessionRequiresSecureDecoder) {
|
||||
getDecoderInfos(mediaCodecSelector, inputFormat, mediaCryptoRequiresSecureDecoder);
|
||||
if (codecInfos.isEmpty() && mediaCryptoRequiresSecureDecoder) {
|
||||
// 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
|
||||
// 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;
|
||||
}
|
||||
|
||||
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.
|
||||
* @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 {
|
||||
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||
if (codecDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||
return false;
|
||||
}
|
||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
||||
@DrmSession.State int drmSessionState = codecDrmSession.getState();
|
||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
||||
throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
|
||||
}
|
||||
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
||||
}
|
||||
@ -1126,13 +1151,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
throw ExoPlaybackException.createForRenderer(
|
||||
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
||||
}
|
||||
pendingDrmSession =
|
||||
DrmSession<FrameworkMediaCrypto> session =
|
||||
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
|
||||
if (pendingDrmSession == drmSession) {
|
||||
drmSessionManager.releaseSession(pendingDrmSession);
|
||||
if (session == sourceDrmSession || session == codecDrmSession) {
|
||||
// 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 {
|
||||
pendingDrmSession = null;
|
||||
setSourceDrmSession(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1143,40 +1171,58 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
|
||||
// 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.
|
||||
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();
|
||||
} else {
|
||||
switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {
|
||||
case KEEP_CODEC_RESULT_NO:
|
||||
drainAndReinitializeCodec();
|
||||
break;
|
||||
case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
|
||||
return;
|
||||
}
|
||||
|
||||
switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {
|
||||
case KEEP_CODEC_RESULT_NO:
|
||||
drainAndReinitializeCodec();
|
||||
break;
|
||||
case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
|
||||
codecFormat = newFormat;
|
||||
updateCodecOperatingRate();
|
||||
if (sourceDrmSession != codecDrmSession) {
|
||||
drainAndUpdateCodecDrmSession();
|
||||
} else {
|
||||
drainAndFlushCodec();
|
||||
}
|
||||
break;
|
||||
case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION:
|
||||
if (codecNeedsReconfigureWorkaround) {
|
||||
drainAndReinitializeCodec();
|
||||
} else {
|
||||
codecReconfigured = true;
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
||||
codecNeedsAdaptationWorkaroundBuffer =
|
||||
codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS
|
||||
|| (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION
|
||||
&& newFormat.width == codecFormat.width
|
||||
&& newFormat.height == codecFormat.height);
|
||||
codecFormat = newFormat;
|
||||
updateCodecOperatingRate();
|
||||
break;
|
||||
case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION:
|
||||
if (codecNeedsReconfigureWorkaround) {
|
||||
drainAndReinitializeCodec();
|
||||
} else {
|
||||
codecReconfigured = true;
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
||||
codecNeedsAdaptationWorkaroundBuffer =
|
||||
codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS
|
||||
|| (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION
|
||||
&& newFormat.width == codecFormat.width
|
||||
&& newFormat.height == codecFormat.height);
|
||||
codecFormat = newFormat;
|
||||
updateCodecOperatingRate();
|
||||
if (sourceDrmSession != codecDrmSession) {
|
||||
drainAndUpdateCodecDrmSession();
|
||||
}
|
||||
break;
|
||||
case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:
|
||||
codecFormat = newFormat;
|
||||
updateCodecOperatingRate();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(); // Never happens.
|
||||
}
|
||||
}
|
||||
break;
|
||||
case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:
|
||||
codecFormat = newFormat;
|
||||
updateCodecOperatingRate();
|
||||
if (sourceDrmSession != codecDrmSession) {
|
||||
drainAndUpdateCodecDrmSession();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(); // Never happens.
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
* buffers have been queued to the codec.
|
||||
@ -1323,8 +1390,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
codecDrainAction = DRAIN_ACTION_REINITIALIZE;
|
||||
} else {
|
||||
// Nothing has been queued to the decoder, so we can re-initialize immediately.
|
||||
releaseCodec();
|
||||
maybeInitCodec();
|
||||
reinitializeCodec();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1528,11 +1594,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
private void processEndOfStream() throws ExoPlaybackException {
|
||||
switch (codecDrainAction) {
|
||||
case DRAIN_ACTION_REINITIALIZE:
|
||||
releaseCodec();
|
||||
maybeInitCodec();
|
||||
reinitializeCodec();
|
||||
break;
|
||||
case DRAIN_ACTION_UPDATE_DRM_SESSION:
|
||||
updateDrmSessionOrReinitializeCodecV23();
|
||||
break;
|
||||
case DRAIN_ACTION_FLUSH:
|
||||
flushOrReinitCodec();
|
||||
flushOrReinitializeCodec();
|
||||
break;
|
||||
case DRAIN_ACTION_NONE:
|
||||
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) {
|
||||
// We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would
|
||||
// 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(
|
||||
DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) {
|
||||
MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16();
|
||||
MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfo();
|
||||
if (adaptiveReconfigurationBytes == 0) {
|
||||
return cryptoInfo;
|
||||
}
|
||||
|
@ -39,7 +39,6 @@ import java.util.regex.Pattern;
|
||||
/**
|
||||
* A utility class for querying the available codecs.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
@SuppressLint("InlinedApi")
|
||||
public final class MediaCodecUtil {
|
||||
|
||||
@ -59,8 +58,6 @@ public final class MediaCodecUtil {
|
||||
|
||||
private static final String TAG = "MediaCodecUtil";
|
||||
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<>();
|
||||
|
||||
@ -312,30 +309,6 @@ public final class MediaCodecUtil {
|
||||
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
|
||||
// https://github.com/google/ExoPlayer/issues/3171.
|
||||
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) {
|
||||
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);
|
||||
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) {
|
||||
Log.w(TAG, "Unknown HEVC level string: " + matcher.group(1));
|
||||
Log.w(TAG, "Unknown HEVC level string: " + levelString);
|
||||
return null;
|
||||
}
|
||||
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 {
|
||||
AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray();
|
||||
AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline);
|
||||
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.mediacodec;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaFormat;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
@ -24,7 +23,6 @@ import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
/** Helper class for configuring {@link MediaFormat} instances. */
|
||||
@TargetApi(16)
|
||||
public final class 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);
|
||||
Collections.sort(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.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Pair;
|
||||
import android.util.SparseIntArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.drm.DrmSessionManager;
|
||||
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.TrackGroup;
|
||||
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.TrackSelection;
|
||||
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.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@ -58,19 +68,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
* <p>A typical usage of DownloadHelper follows these steps:
|
||||
*
|
||||
* <ol>
|
||||
* <li>Construct the download helper with information about the {@link RenderersFactory renderers}
|
||||
* and {@link DefaultTrackSelector.Parameters parameters} for track selection.
|
||||
* <li>Build the helper using one of the {@code forXXX} methods.
|
||||
* <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
|
||||
* #getTrackSelections(int, int)}, and make adjustments using {@link
|
||||
* #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link
|
||||
* #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>
|
||||
*
|
||||
* @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
|
||||
@ -87,7 +95,7 @@ public abstract class DownloadHelper<T> {
|
||||
*
|
||||
* @param helper The reporting {@link DownloadHelper}.
|
||||
*/
|
||||
void onPrepared(DownloadHelper<?> helper);
|
||||
void onPrepared(DownloadHelper helper);
|
||||
|
||||
/**
|
||||
* Called when preparation fails.
|
||||
@ -95,18 +103,222 @@ public abstract class DownloadHelper<T> {
|
||||
* @param helper The reporting {@link DownloadHelper}.
|
||||
* @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 Uri uri;
|
||||
@Nullable private final String cacheKey;
|
||||
@Nullable private final MediaSource mediaSource;
|
||||
private final DefaultTrackSelector trackSelector;
|
||||
private final RendererCapabilities[] rendererCapabilities;
|
||||
private final SparseIntArray scratchSet;
|
||||
|
||||
private int currentTrackSelectionPeriodIndex;
|
||||
@Nullable private T manifest;
|
||||
private boolean isPreparedWithMedia;
|
||||
private @MonotonicNonNull Callback callback;
|
||||
private @MonotonicNonNull Handler callbackHandler;
|
||||
private @MonotonicNonNull MediaPreparer mediaPreparer;
|
||||
private TrackGroupArray @MonotonicNonNull [] trackGroupArrays;
|
||||
private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos;
|
||||
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 uri A {@link Uri}.
|
||||
* @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
|
||||
* 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.
|
||||
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
|
||||
* {@code renderersFactory}.
|
||||
*/
|
||||
public DownloadHelper(
|
||||
String downloadType,
|
||||
Uri uri,
|
||||
@Nullable String cacheKey,
|
||||
@Nullable MediaSource mediaSource,
|
||||
DefaultTrackSelector.Parameters trackSelectorParameters,
|
||||
RenderersFactory renderersFactory,
|
||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
||||
RendererCapabilities[] rendererCapabilities) {
|
||||
this.downloadType = downloadType;
|
||||
this.uri = uri;
|
||||
this.cacheKey = cacheKey;
|
||||
this.mediaSource = mediaSource;
|
||||
this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory());
|
||||
this.rendererCapabilities = Util.getRendererCapabilities(renderersFactory, drmSessionManager);
|
||||
this.rendererCapabilities = rendererCapabilities;
|
||||
this.scratchSet = new SparseIntArray();
|
||||
trackSelector.setParameters(trackSelectorParameters);
|
||||
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
|
||||
* 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.
|
||||
* @throws IllegalStateException If the download helper has already been prepared.
|
||||
*/
|
||||
public final void prepare(Callback callback) {
|
||||
Handler handler =
|
||||
public void prepare(Callback callback) {
|
||||
Assertions.checkState(this.callback == null);
|
||||
this.callback = callback;
|
||||
callbackHandler =
|
||||
new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper());
|
||||
new Thread(
|
||||
() -> {
|
||||
try {
|
||||
manifest = loadManifest(uri);
|
||||
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();
|
||||
if (mediaSource != null) {
|
||||
mediaPreparer = new MediaPreparer(mediaSource, /* downloadHelper= */ this);
|
||||
} else {
|
||||
callbackHandler.post(() -> callback.onPrepared(this));
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the manifest. Must not be called until after preparation completes. */
|
||||
public final T getManifest() {
|
||||
Assertions.checkNotNull(manifest);
|
||||
return manifest;
|
||||
/** Releases the helper and all resources it is holding. */
|
||||
public void release() {
|
||||
if (mediaPreparer != null) {
|
||||
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
|
||||
* preparation completes.
|
||||
*/
|
||||
public final int getPeriodCount() {
|
||||
Assertions.checkNotNull(trackGroupArrays);
|
||||
public int getPeriodCount() {
|
||||
if (mediaSource == null) {
|
||||
return 0;
|
||||
}
|
||||
assertPreparedWithMedia();
|
||||
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
|
||||
* content.
|
||||
*/
|
||||
public final TrackGroupArray getTrackGroups(int periodIndex) {
|
||||
Assertions.checkNotNull(trackGroupArrays);
|
||||
public TrackGroupArray getTrackGroups(int periodIndex) {
|
||||
assertPreparedWithMedia();
|
||||
return trackGroupArrays[periodIndex];
|
||||
}
|
||||
|
||||
@ -210,8 +429,8 @@ public abstract class DownloadHelper<T> {
|
||||
* @param periodIndex The period index.
|
||||
* @return The {@link MappedTrackInfo} for the period.
|
||||
*/
|
||||
public final MappedTrackInfo getMappedTrackInfo(int periodIndex) {
|
||||
Assertions.checkNotNull(mappedTrackInfos);
|
||||
public MappedTrackInfo getMappedTrackInfo(int periodIndex) {
|
||||
assertPreparedWithMedia();
|
||||
return mappedTrackInfos[periodIndex];
|
||||
}
|
||||
|
||||
@ -223,8 +442,8 @@ public abstract class DownloadHelper<T> {
|
||||
* @param rendererIndex The renderer index.
|
||||
* @return A list of selected {@link TrackSelection track selections}.
|
||||
*/
|
||||
public final List<TrackSelection> getTrackSelections(int periodIndex, int rendererIndex) {
|
||||
Assertions.checkNotNull(immutableTrackSelectionsByPeriodAndRenderer);
|
||||
public List<TrackSelection> getTrackSelections(int periodIndex, int rendererIndex) {
|
||||
assertPreparedWithMedia();
|
||||
return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
|
||||
}
|
||||
|
||||
@ -234,8 +453,8 @@ public abstract class DownloadHelper<T> {
|
||||
*
|
||||
* @param periodIndex The period index for which track selections are cleared.
|
||||
*/
|
||||
public final void clearTrackSelections(int periodIndex) {
|
||||
Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
|
||||
public void clearTrackSelections(int periodIndex) {
|
||||
assertPreparedWithMedia();
|
||||
for (int i = 0; i < rendererCapabilities.length; i++) {
|
||||
trackSelectionsByPeriodAndRenderer[periodIndex][i].clear();
|
||||
}
|
||||
@ -249,7 +468,7 @@ public abstract class DownloadHelper<T> {
|
||||
* @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
|
||||
* selection of tracks.
|
||||
*/
|
||||
public final void replaceTrackSelections(
|
||||
public void replaceTrackSelections(
|
||||
int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
|
||||
clearTrackSelections(periodIndex);
|
||||
addTrackSelection(periodIndex, trackSelectorParameters);
|
||||
@ -263,14 +482,71 @@ public abstract class DownloadHelper<T> {
|
||||
* @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
|
||||
* selection of tracks.
|
||||
*/
|
||||
public final void addTrackSelection(
|
||||
public void addTrackSelection(
|
||||
int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
|
||||
Assertions.checkNotNull(trackGroupArrays);
|
||||
Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
|
||||
assertPreparedWithMedia();
|
||||
trackSelector.setParameters(trackSelectorParameters);
|
||||
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
|
||||
* after preparation completes.
|
||||
@ -278,27 +554,22 @@ public abstract class DownloadHelper<T> {
|
||||
* @param data Application provided data to store in {@link DownloadAction#data}.
|
||||
* @return The built {@link DownloadAction}.
|
||||
*/
|
||||
public final DownloadAction getDownloadAction(@Nullable byte[] data) {
|
||||
Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
|
||||
Assertions.checkNotNull(trackGroupArrays);
|
||||
public DownloadAction getDownloadAction(@Nullable byte[] data) {
|
||||
if (mediaSource == null) {
|
||||
return DownloadAction.createDownloadAction(
|
||||
downloadType, uri, /* keys= */ Collections.emptyList(), cacheKey, data);
|
||||
}
|
||||
assertPreparedWithMedia();
|
||||
List<StreamKey> streamKeys = new ArrayList<>();
|
||||
List<TrackSelection> allSelections = new ArrayList<>();
|
||||
int periodCount = trackSelectionsByPeriodAndRenderer.length;
|
||||
for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
|
||||
allSelections.clear();
|
||||
int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length;
|
||||
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
|
||||
List<TrackSelection> trackSelectionList =
|
||||
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));
|
||||
}
|
||||
}
|
||||
allSelections.addAll(trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]);
|
||||
}
|
||||
streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections));
|
||||
}
|
||||
return DownloadAction.createDownloadAction(downloadType, uri, streamKeys, cacheKey, data);
|
||||
}
|
||||
@ -308,40 +579,18 @@ public abstract class DownloadHelper<T> {
|
||||
*
|
||||
* @return The built {@link DownloadAction}.
|
||||
*/
|
||||
public final DownloadAction getRemoveAction() {
|
||||
public DownloadAction getRemoveAction() {
|
||||
return DownloadAction.createRemoveAction(downloadType, uri, cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// Initialization of array of Lists.
|
||||
@SuppressWarnings("unchecked")
|
||||
@EnsuresNonNull("trackSelectionsByPeriodAndRenderer")
|
||||
private void initializeTrackSelectionLists(int periodCount, int rendererCount) {
|
||||
private void onMediaPrepared() {
|
||||
Assertions.checkNotNull(mediaPreparer);
|
||||
Assertions.checkNotNull(mediaPreparer.mediaPeriods);
|
||||
Assertions.checkNotNull(mediaPreparer.timeline);
|
||||
int periodCount = mediaPreparer.mediaPeriods.length;
|
||||
int rendererCount = rendererCapabilities.length;
|
||||
trackSelectionsByPeriodAndRenderer =
|
||||
(List<TrackSelection>[][]) new List<?>[periodCount][rendererCount];
|
||||
immutableTrackSelectionsByPeriodAndRenderer =
|
||||
@ -353,6 +602,49 @@ public abstract class DownloadHelper<T> {
|
||||
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.
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
@RequiresNonNull({"trackGroupArrays", "trackSelectionsByPeriodAndRenderer"})
|
||||
@RequiresNonNull({
|
||||
"trackGroupArrays",
|
||||
"trackSelectionsByPeriodAndRenderer",
|
||||
"mediaPreparer",
|
||||
"mediaPreparer.timeline"
|
||||
})
|
||||
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 {
|
||||
TrackSelectorResult trackSelectorResult =
|
||||
trackSelector.selectTracks(
|
||||
rendererCapabilities,
|
||||
trackGroupArrays[periodIndex],
|
||||
dummyMediaPeriodId,
|
||||
dummyTimeline);
|
||||
new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)),
|
||||
mediaPreparer.timeline);
|
||||
for (int i = 0; i < trackSelectorResult.length; i++) {
|
||||
TrackSelection newSelection = trackSelectorResult.selections.get(i);
|
||||
if (newSelection == null) {
|
||||
continue;
|
||||
}
|
||||
List<TrackSelection> existingSelectionList =
|
||||
trackSelectionsByPeriodAndRenderer[currentTrackSelectionPeriodIndex][i];
|
||||
trackSelectionsByPeriodAndRenderer[periodIndex][i];
|
||||
boolean mergedWithExistingSelection = false;
|
||||
for (int j = 0; j < existingSelectionList.size(); 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 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_RESTARTING;
|
||||
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_STOPPED;
|
||||
import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_MANUAL;
|
||||
import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_REQUIREMENTS_NOT_MET;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.ConditionVariable;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.support.annotation.Nullable;
|
||||
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.Log;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
@ -74,22 +81,55 @@ public final class DownloadManager {
|
||||
* @param downloadManager The reporting instance.
|
||||
*/
|
||||
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. */
|
||||
public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1;
|
||||
/** The default minimum number of times a download must be retried before failing. */
|
||||
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 boolean DEBUG = false;
|
||||
|
||||
private final int maxActiveDownloads;
|
||||
private final int maxSimultaneousDownloads;
|
||||
private final int minRetryCount;
|
||||
private final Context context;
|
||||
private final ActionFile actionFile;
|
||||
private final DownloaderFactory downloaderFactory;
|
||||
private final ArrayList<Download> downloads;
|
||||
private final ArrayList<Download> activeDownloads;
|
||||
private final HashMap<Download, DownloadThread> activeDownloads;
|
||||
private final Handler handler;
|
||||
private final HandlerThread fileIOThread;
|
||||
private final Handler fileIOHandler;
|
||||
@ -98,40 +138,55 @@ public final class DownloadManager {
|
||||
|
||||
private boolean initialized;
|
||||
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}.
|
||||
*
|
||||
* @param context Any context.
|
||||
* @param actionFile The file in which active actions are saved.
|
||||
* @param downloaderFactory A factory for creating {@link Downloader}s.
|
||||
*/
|
||||
public DownloadManager(File actionFile, DownloaderFactory downloaderFactory) {
|
||||
public DownloadManager(Context context, File actionFile, DownloaderFactory downloaderFactory) {
|
||||
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}.
|
||||
*
|
||||
* @param context Any context.
|
||||
* @param actionFile The file in which active actions are saved.
|
||||
* @param downloaderFactory A factory for creating {@link Downloader}s.
|
||||
* @param maxSimultaneousDownloads The maximum number of simultaneous downloads.
|
||||
* @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(
|
||||
Context context,
|
||||
File actionFile,
|
||||
DownloaderFactory downloaderFactory,
|
||||
int maxSimultaneousDownloads,
|
||||
int minRetryCount) {
|
||||
int minRetryCount,
|
||||
Requirements requirements) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.actionFile = new ActionFile(actionFile);
|
||||
this.downloaderFactory = downloaderFactory;
|
||||
this.maxActiveDownloads = maxSimultaneousDownloads;
|
||||
this.maxSimultaneousDownloads = maxSimultaneousDownloads;
|
||||
this.minRetryCount = minRetryCount;
|
||||
this.stickyStopFlags = STOP_FLAG_STOPPED | STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY;
|
||||
|
||||
stopFlags = STOP_FLAG_MANUAL;
|
||||
downloads = new ArrayList<>();
|
||||
activeDownloads = new ArrayList<>();
|
||||
activeDownloads = new HashMap<>();
|
||||
|
||||
Looper looper = Looper.myLooper();
|
||||
if (looper == null) {
|
||||
@ -146,10 +201,30 @@ public final class DownloadManager {
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
actionQueue = new ArrayDeque<>();
|
||||
|
||||
setNotMetRequirements(watchRequirements(requirements));
|
||||
loadActions();
|
||||
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}.
|
||||
*
|
||||
@ -168,33 +243,35 @@ public final class DownloadManager {
|
||||
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() {
|
||||
clearStopFlags(STOP_FLAG_STOPPED);
|
||||
logd("manual stopped is cancelled");
|
||||
manualStopReason = 0;
|
||||
stopFlags &= ~STOP_FLAG_MANUAL;
|
||||
for (int i = 0; i < downloads.size(); i++) {
|
||||
downloads.get(i).clearManualStopReason();
|
||||
}
|
||||
}
|
||||
|
||||
/** Stops all of the downloads. Call {@link #startDownloads()} to restart downloads. */
|
||||
/** Signals all downloads to stop. Call {@link #startDownloads()} to let them to be started. */
|
||||
public void stopDownloads() {
|
||||
setStopFlags(STOP_FLAG_STOPPED);
|
||||
stopDownloads(0);
|
||||
}
|
||||
|
||||
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++) {
|
||||
downloads.get(i).updateStopFlags(flags, values);
|
||||
}
|
||||
logdFlags("Sticky stop flags are updated", updatedStickyStopFlags);
|
||||
/**
|
||||
* 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. */
|
||||
public boolean isIdle() {
|
||||
Assertions.checkState(!released);
|
||||
if (!initialized) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < downloads.size(); i++) {
|
||||
if (!downloads.get(i).isIdle()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return initialized && activeDownloads.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -276,8 +345,11 @@ public final class DownloadManager {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
setStopFlags(STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY);
|
||||
released = true;
|
||||
stopAllDownloadThreads();
|
||||
if (requirementsWatcher != null) {
|
||||
requirementsWatcher.stop();
|
||||
}
|
||||
final ConditionVariable fileIOFinishedCondition = new ConditionVariable();
|
||||
fileIOHandler.post(fileIOFinishedCondition::open);
|
||||
fileIOFinishedCondition.block();
|
||||
@ -293,20 +365,11 @@ public final class DownloadManager {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Download download =
|
||||
new Download(this, downloaderFactory, action, minRetryCount, stickyStopFlags);
|
||||
Download download = new Download(this, action, stopFlags, notMetRequirements, manualStopReason);
|
||||
downloads.add(download);
|
||||
logd("Download is added", download);
|
||||
}
|
||||
|
||||
private void maybeStartDownload(Download download) {
|
||||
if (activeDownloads.size() < maxActiveDownloads) {
|
||||
if (download.start()) {
|
||||
activeDownloads.add(download);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeNotifyListenersIdle() {
|
||||
if (!isIdle()) {
|
||||
return;
|
||||
@ -321,21 +384,11 @@ public final class DownloadManager {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
boolean idle = download.isIdle();
|
||||
if (idle) {
|
||||
activeDownloads.remove(download);
|
||||
}
|
||||
notifyListenersDownloadStateChange(download);
|
||||
if (download.isFinished()) {
|
||||
downloads.remove(download);
|
||||
saveActions();
|
||||
}
|
||||
if (idle) {
|
||||
for (int i = 0; i < downloads.size(); i++) {
|
||||
maybeStartDownload(downloads.get(i));
|
||||
}
|
||||
maybeNotifyListenersIdle();
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
fileIOHandler.post(
|
||||
() -> {
|
||||
@ -377,7 +451,9 @@ public final class DownloadManager {
|
||||
for (Listener listener : listeners) {
|
||||
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 final String id;
|
||||
private final DownloadManager downloadManager;
|
||||
private final DownloaderFactory downloaderFactory;
|
||||
private final int minRetryCount;
|
||||
private final long startTimeMs;
|
||||
private final ArrayDeque<DownloadAction> actionQueue;
|
||||
/** The current state of the download. */
|
||||
@DownloadState.State private int state;
|
||||
|
||||
@MonotonicNonNull private Downloader downloader;
|
||||
@MonotonicNonNull private DownloadThread downloadThread;
|
||||
@DownloadState.State private int state;
|
||||
@MonotonicNonNull @DownloadState.FailureReason private int failureReason;
|
||||
@DownloadState.StopFlags private int stopFlags;
|
||||
@Requirements.RequirementFlags private int notMetRequirements;
|
||||
private int manualStopReason;
|
||||
|
||||
private Download(
|
||||
DownloadManager downloadManager,
|
||||
DownloaderFactory downloaderFactory,
|
||||
DownloadAction action,
|
||||
int minRetryCount,
|
||||
int stopFlags) {
|
||||
@DownloadState.StopFlags int stopFlags,
|
||||
@Requirements.RequirementFlags int notMetRequirements,
|
||||
int manualStopReason) {
|
||||
this.id = action.id;
|
||||
this.downloadManager = downloadManager;
|
||||
this.downloaderFactory = downloaderFactory;
|
||||
this.minRetryCount = minRetryCount;
|
||||
this.notMetRequirements = notMetRequirements;
|
||||
this.manualStopReason = manualStopReason;
|
||||
this.stopFlags = stopFlags;
|
||||
this.startTimeMs = System.currentTimeMillis();
|
||||
actionQueue = new ArrayDeque<>();
|
||||
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) {
|
||||
@ -472,12 +636,9 @@ public final class DownloadManager {
|
||||
setState(STATE_REMOVING);
|
||||
}
|
||||
} else if (!action.equals(updatedAction)) {
|
||||
if (state == STATE_DOWNLOADING) {
|
||||
stopDownloadThread();
|
||||
} else {
|
||||
Assertions.checkState(state == STATE_QUEUED || state == STATE_STOPPED);
|
||||
initialize(/* restart= */ false);
|
||||
}
|
||||
Assertions.checkState(
|
||||
state == STATE_DOWNLOADING || state == STATE_QUEUED || state == STATE_STOPPED);
|
||||
initialize();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -486,6 +647,7 @@ public final class DownloadManager {
|
||||
float downloadPercentage = C.PERCENTAGE_UNSET;
|
||||
long downloadedBytes = 0;
|
||||
long totalBytes = C.LENGTH_UNSET;
|
||||
Downloader downloader = downloadManager.getDownloader(this);
|
||||
if (downloader != null) {
|
||||
downloadPercentage = downloader.getDownloadPercentage();
|
||||
downloadedBytes = downloader.getDownloadedBytes();
|
||||
@ -503,6 +665,8 @@ public final class DownloadManager {
|
||||
totalBytes,
|
||||
failureReason,
|
||||
stopFlags,
|
||||
notMetRequirements,
|
||||
manualStopReason,
|
||||
startTimeMs,
|
||||
/* updateTimeMs= */ System.currentTimeMillis(),
|
||||
action.keys.toArray(new StreamKey[0]),
|
||||
@ -522,123 +686,119 @@ public final class DownloadManager {
|
||||
return id + ' ' + DownloadState.getStateString(state);
|
||||
}
|
||||
|
||||
public boolean start() {
|
||||
if (state != STATE_QUEUED) {
|
||||
return false;
|
||||
public void start() {
|
||||
if (state == STATE_QUEUED || state == STATE_DOWNLOADING) {
|
||||
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) {
|
||||
updateStopFlags(flags, flags);
|
||||
public void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) {
|
||||
this.notMetRequirements = notMetRequirements;
|
||||
updateStopFlags(STOP_FLAG_REQUIREMENTS_NOT_MET, /* setFlags= */ notMetRequirements != 0);
|
||||
}
|
||||
|
||||
public void clearStopFlags(int flags) {
|
||||
updateStopFlags(flags, 0);
|
||||
public void setManualStopReason(int manualStopReason) {
|
||||
this.manualStopReason = manualStopReason;
|
||||
updateStopFlags(STOP_FLAG_MANUAL, /* setFlags= */ true);
|
||||
}
|
||||
|
||||
public void updateStopFlags(int flags, int values) {
|
||||
stopFlags = (values & flags) | (stopFlags & ~flags);
|
||||
public void clearManualStopReason() {
|
||||
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 (state == STATE_DOWNLOADING) {
|
||||
stopDownloadThread();
|
||||
} else if (state == STATE_QUEUED) {
|
||||
if (state == STATE_DOWNLOADING || state == STATE_QUEUED) {
|
||||
downloadManager.stopDownloadThread(this);
|
||||
setState(STATE_STOPPED);
|
||||
}
|
||||
} else if (state == STATE_STOPPED) {
|
||||
startOrQueue(/* restart= */ false);
|
||||
startOrQueue();
|
||||
}
|
||||
}
|
||||
|
||||
private void initialize(boolean restart) {
|
||||
private void initialize() {
|
||||
DownloadAction action = actionQueue.peek();
|
||||
if (action.isRemoveAction) {
|
||||
if (!downloadManager.released) {
|
||||
startDownloadThread(action);
|
||||
}
|
||||
int result = downloadManager.startDownloadThread(this, 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);
|
||||
} else if (stopFlags != 0) {
|
||||
setState(STATE_STOPPED);
|
||||
} else {
|
||||
startOrQueue(restart);
|
||||
startOrQueue();
|
||||
}
|
||||
}
|
||||
|
||||
private void startOrQueue(boolean restart) {
|
||||
// Set to queued state but don't notify listeners until we make sure we can't start now.
|
||||
state = STATE_QUEUED;
|
||||
if (restart) {
|
||||
start();
|
||||
private void startOrQueue() {
|
||||
DownloadAction action = Assertions.checkNotNull(actionQueue.peek());
|
||||
Assertions.checkState(!action.isRemoveAction);
|
||||
@StartThreadResults int result = downloadManager.startDownloadThread(this, action);
|
||||
Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH);
|
||||
if (result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION) {
|
||||
setState(STATE_DOWNLOADING);
|
||||
} else {
|
||||
downloadManager.maybeStartDownload(this);
|
||||
}
|
||||
if (state == STATE_QUEUED) {
|
||||
downloadManager.onDownloadStateChange(this);
|
||||
setState(STATE_QUEUED);
|
||||
}
|
||||
}
|
||||
|
||||
private void setState(@DownloadState.State int newState) {
|
||||
state = newState;
|
||||
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() {
|
||||
Assertions.checkNotNull(downloadThread).cancel();
|
||||
}
|
||||
|
||||
private void onDownloadThreadStopped(@Nullable Throwable finalError) {
|
||||
failureReason = FAILURE_REASON_NONE;
|
||||
if (!downloadThread.isCanceled) {
|
||||
if (finalError != null && state != STATE_REMOVING && state != STATE_RESTARTING) {
|
||||
failureReason = FAILURE_REASON_UNKNOWN;
|
||||
setState(STATE_FAILED);
|
||||
return;
|
||||
}
|
||||
if (actionQueue.size() == 1) {
|
||||
if (state == STATE_REMOVING) {
|
||||
setState(STATE_REMOVED);
|
||||
} else {
|
||||
Assertions.checkState(state == STATE_DOWNLOADING);
|
||||
setState(STATE_COMPLETED);
|
||||
}
|
||||
return;
|
||||
}
|
||||
actionQueue.remove();
|
||||
if (state != newState) {
|
||||
state = newState;
|
||||
downloadManager.onDownloadStateChange(this);
|
||||
}
|
||||
initialize(/* restart= */ state == STATE_DOWNLOADING);
|
||||
}
|
||||
|
||||
private void onDownloadThreadStopped(boolean isCanceled, @Nullable Throwable error) {
|
||||
failureReason = FAILURE_REASON_NONE;
|
||||
if (isCanceled) {
|
||||
if (!isIdle()) {
|
||||
downloadManager.startDownloadThread(this, actionQueue.peek());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (error != null && state == STATE_DOWNLOADING) {
|
||||
failureReason = FAILURE_REASON_UNKNOWN;
|
||||
setState(STATE_FAILED);
|
||||
return;
|
||||
}
|
||||
if (actionQueue.size() == 1) {
|
||||
if (state == STATE_REMOVING) {
|
||||
setState(STATE_REMOVED);
|
||||
} else {
|
||||
Assertions.checkState(state == STATE_DOWNLOADING);
|
||||
setState(STATE_COMPLETED);
|
||||
}
|
||||
return;
|
||||
}
|
||||
actionQueue.remove();
|
||||
initialize();
|
||||
}
|
||||
}
|
||||
|
||||
private static class DownloadThread implements Runnable {
|
||||
private class DownloadThread implements Runnable {
|
||||
|
||||
private final Download download;
|
||||
private final Downloader downloader;
|
||||
private final boolean remove;
|
||||
private final int minRetryCount;
|
||||
private final Handler callbackHandler;
|
||||
private final boolean isRemoveThread;
|
||||
private final Thread thread;
|
||||
private volatile boolean isCanceled;
|
||||
|
||||
private DownloadThread(
|
||||
Download download,
|
||||
Downloader downloader,
|
||||
boolean remove,
|
||||
int minRetryCount,
|
||||
Handler callbackHandler) {
|
||||
private DownloadThread(Download download, Downloader downloader, boolean isRemoveThread) {
|
||||
this.download = download;
|
||||
this.downloader = downloader;
|
||||
this.remove = remove;
|
||||
this.minRetryCount = minRetryCount;
|
||||
this.callbackHandler = callbackHandler;
|
||||
this.isRemoveThread = isRemoveThread;
|
||||
thread = new Thread(this);
|
||||
thread.start();
|
||||
}
|
||||
@ -653,10 +813,10 @@ public final class DownloadManager {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
logd("Download is started", download);
|
||||
logd("Download started", download);
|
||||
Throwable error = null;
|
||||
try {
|
||||
if (remove) {
|
||||
if (isRemoveThread) {
|
||||
downloader.remove();
|
||||
} else {
|
||||
int errorCount = 0;
|
||||
@ -686,11 +846,12 @@ public final class DownloadManager {
|
||||
error = e;
|
||||
}
|
||||
final Throwable finalError = error;
|
||||
callbackHandler.post(() -> download.onDownloadThreadStopped(isCanceled ? null : finalError));
|
||||
handler.post(() -> onDownloadThreadStopped(this, finalError));
|
||||
}
|
||||
|
||||
private int getRetryDelayMillis(int errorCount) {
|
||||
return Math.min((errorCount - 1) * 1000, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,8 +25,8 @@ import android.os.Looper;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
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.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.NotificationUtil;
|
||||
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. */
|
||||
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. */
|
||||
private static final String 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 boolean DEBUG = false;
|
||||
|
||||
// Keep the requirements helper 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
|
||||
// allow downloads the resume more quickly than when relying on the scheduler alone.
|
||||
private static final HashMap<Class<? extends DownloadService>, RequirementsHelper>
|
||||
requirementsHelpers = new HashMap<>();
|
||||
private static final Requirements DEFAULT_REQUIREMENTS =
|
||||
new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
|
||||
// Keep DownloadManagerListeners for each DownloadService as long as there are downloads (and the
|
||||
// process is running). This allows DownloadService to restart when there's no scheduler.
|
||||
private static final HashMap<Class<? extends DownloadService>, DownloadManagerHelper>
|
||||
downloadManagerListeners = new HashMap<>();
|
||||
|
||||
private final @Nullable ForegroundNotificationUpdater foregroundNotificationUpdater;
|
||||
private final @Nullable String channelId;
|
||||
private final @StringRes int channelName;
|
||||
|
||||
private DownloadManager downloadManager;
|
||||
private DownloadManagerListener downloadManagerListener;
|
||||
private int lastStartId;
|
||||
private boolean startedInForeground;
|
||||
private boolean taskRemoved;
|
||||
@ -227,9 +219,16 @@ public abstract class DownloadService extends Service {
|
||||
NotificationUtil.createNotificationChannel(
|
||||
this, channelId, channelName, NotificationUtil.IMPORTANCE_LOW);
|
||||
}
|
||||
downloadManager = getDownloadManager();
|
||||
downloadManagerListener = new DownloadManagerListener();
|
||||
downloadManager.addListener(downloadManagerListener);
|
||||
Class<? extends DownloadService> clazz = getClass();
|
||||
DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz);
|
||||
if (downloadManagerHelper == null) {
|
||||
downloadManagerHelper =
|
||||
new DownloadManagerHelper(
|
||||
getApplicationContext(), getDownloadManager(), getScheduler(), clazz);
|
||||
downloadManagerListeners.put(clazz, downloadManagerHelper);
|
||||
}
|
||||
downloadManager = downloadManagerHelper.downloadManager;
|
||||
downloadManagerHelper.attachService(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -264,22 +263,11 @@ public abstract class DownloadService extends Service {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ACTION_RELOAD_REQUIREMENTS:
|
||||
stopWatchingRequirements();
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Ignoring unrecognized action: " + intentAction);
|
||||
break;
|
||||
}
|
||||
|
||||
Requirements requirements = getRequirements();
|
||||
if (requirements.checkRequirements(this)) {
|
||||
downloadManager.startDownloads();
|
||||
} else {
|
||||
downloadManager.stopDownloads();
|
||||
}
|
||||
maybeStartWatchingRequirements(requirements);
|
||||
|
||||
if (downloadManager.isIdle()) {
|
||||
stop();
|
||||
}
|
||||
@ -295,11 +283,12 @@ public abstract class DownloadService extends Service {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
logd("onDestroy");
|
||||
DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(getClass());
|
||||
boolean unschedule = downloadManager.getDownloadCount() <= 0;
|
||||
downloadManagerHelper.detachService(this, unschedule);
|
||||
if (foregroundNotificationUpdater != null) {
|
||||
foregroundNotificationUpdater.stopPeriodicUpdates();
|
||||
}
|
||||
downloadManager.removeListener(downloadManagerListener);
|
||||
maybeStopWatchingRequirements();
|
||||
}
|
||||
|
||||
/** 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
|
||||
* life cycle of the service. The service will call {@link DownloadManager#startDownloads()} and
|
||||
* {@link DownloadManager#stopDownloads} as necessary when requirements returned by {@link
|
||||
* #getRequirements()} are met or stop being met.
|
||||
* life cycle of the process.
|
||||
*/
|
||||
protected abstract DownloadManager getDownloadManager();
|
||||
|
||||
@ -324,14 +311,6 @@ public abstract class DownloadService extends Service {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@ -363,32 +342,16 @@ public abstract class DownloadService extends Service {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void maybeStartWatchingRequirements(Requirements requirements) {
|
||||
if (downloadManager.getDownloadCount() == 0) {
|
||||
return;
|
||||
}
|
||||
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");
|
||||
private void notifyDownloadStateChange(DownloadState downloadState) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -420,33 +383,6 @@ public abstract class DownloadService extends Service {
|
||||
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 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 Requirements requirements;
|
||||
private final @Nullable Scheduler scheduler;
|
||||
private final DownloadManager downloadManager;
|
||||
@Nullable private final Scheduler scheduler;
|
||||
private final Class<? extends DownloadService> serviceClass;
|
||||
private final RequirementsWatcher requirementsWatcher;
|
||||
@Nullable private DownloadService downloadService;
|
||||
|
||||
private RequirementsHelper(
|
||||
private DownloadManagerHelper(
|
||||
Context context,
|
||||
Requirements requirements,
|
||||
DownloadManager downloadManager,
|
||||
@Nullable Scheduler scheduler,
|
||||
Class<? extends DownloadService> serviceClass) {
|
||||
this.context = context;
|
||||
this.requirements = requirements;
|
||||
this.downloadManager = downloadManager;
|
||||
this.scheduler = scheduler;
|
||||
this.serviceClass = serviceClass;
|
||||
requirementsWatcher = new RequirementsWatcher(context, this, requirements);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
requirementsWatcher.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
requirementsWatcher.stop();
|
||||
downloadManager.addListener(this);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requirementsMet(RequirementsWatcher requirementsWatcher) {
|
||||
try {
|
||||
notifyService();
|
||||
} catch (Exception e) {
|
||||
/* If we can't notify the service, don't stop the scheduler. */
|
||||
return;
|
||||
}
|
||||
if (scheduler != null) {
|
||||
scheduler.cancel();
|
||||
public void onInitialized(DownloadManager downloadManager) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadStateChanged(
|
||||
DownloadManager downloadManager, DownloadState downloadState) {
|
||||
if (downloadService != null) {
|
||||
downloadService.notifyDownloadStateChange(downloadState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requirementsNotMet(RequirementsWatcher requirementsWatcher) {
|
||||
try {
|
||||
notifyService();
|
||||
} catch (Exception e) {
|
||||
/* Do nothing. The service isn't running anyway. */
|
||||
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 {
|
||||
Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);
|
||||
context.startService(intent);
|
||||
} catch (IllegalStateException e) {
|
||||
/* startService fails if the app is in the background then don't stop the scheduler. */
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (scheduler != null) {
|
||||
setSchedulerEnabled(/* enabled= */ !requirementsMet, requirements);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSchedulerEnabled(boolean enabled, Requirements requirements) {
|
||||
if (!enabled) {
|
||||
scheduler.cancel();
|
||||
} else {
|
||||
String servicePackage = context.getPackageName();
|
||||
boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART);
|
||||
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
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.offline;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.support.annotation.Nullable;
|
||||
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 java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
||||
/** Represents state of a download. */
|
||||
public final class DownloadState {
|
||||
@ -74,19 +77,19 @@ public final class DownloadState {
|
||||
public static final int FAILURE_REASON_UNKNOWN = 1;
|
||||
|
||||
/**
|
||||
* Download stop flags. Possible flag values are {@link #STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY} and
|
||||
* {@link #STOP_FLAG_STOPPED}.
|
||||
* Download stop flags. Possible flag values are {@link #STOP_FLAG_MANUAL} and {@link
|
||||
* #STOP_FLAG_REQUIREMENTS_NOT_MET}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(
|
||||
flag = true,
|
||||
value = {STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY, STOP_FLAG_STOPPED})
|
||||
value = {STOP_FLAG_MANUAL, STOP_FLAG_REQUIREMENTS_NOT_MET})
|
||||
public @interface StopFlags {}
|
||||
/** Download can't be started as the manager isn't ready. */
|
||||
public static final int STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY = 1;
|
||||
/** All downloads are stopped by the application. */
|
||||
public static final int STOP_FLAG_STOPPED = 1 << 1;
|
||||
/** Download is stopped by the application. */
|
||||
public static final int STOP_FLAG_MANUAL = 1;
|
||||
/** Download is stopped as the requirements are not met. */
|
||||
public static final int STOP_FLAG_REQUIREMENTS_NOT_MET = 1 << 1;
|
||||
|
||||
/** Returns the state string for the given state value. */
|
||||
public static String getStateString(@State int state) {
|
||||
@ -154,7 +157,40 @@ public final class DownloadState {
|
||||
*/
|
||||
@FailureReason public final int failureReason;
|
||||
/** 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(
|
||||
String id,
|
||||
@ -167,28 +203,91 @@ public final class DownloadState {
|
||||
long totalBytes,
|
||||
@FailureReason int failureReason,
|
||||
@StopFlags int stopFlags,
|
||||
@RequirementFlags int notMetRequirements,
|
||||
int manualStopReason,
|
||||
long startTimeMs,
|
||||
long updateTimeMs,
|
||||
StreamKey[] streamKeys,
|
||||
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(
|
||||
failureReason == FAILURE_REASON_NONE ? state != STATE_FAILED : state == STATE_FAILED);
|
||||
// TODO enable this when we start changing state immediately
|
||||
// Assertions.checkState(stopFlags == 0 || (state != STATE_DOWNLOADING && state !=
|
||||
// STATE_QUEUED));
|
||||
((stopFlags & STOP_FLAG_REQUIREMENTS_NOT_MET) == 0) == (notMetRequirements == 0));
|
||||
Assertions.checkState(((stopFlags & STOP_FLAG_MANUAL) != 0) || (manualStopReason == 0));
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.uri = uri;
|
||||
this.cacheKey = cacheKey;
|
||||
this.streamKeys = streamKeys;
|
||||
this.customMetadata = customMetadata;
|
||||
this.state = state;
|
||||
this.downloadPercentage = downloadPercentage;
|
||||
this.downloadedBytes = downloadedBytes;
|
||||
this.totalBytes = totalBytes;
|
||||
this.failureReason = failureReason;
|
||||
this.stopFlags = stopFlags;
|
||||
this.notMetRequirements = notMetRequirements;
|
||||
this.manualStopReason = manualStopReason;
|
||||
this.startTimeMs = startTimeMs;
|
||||
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
|
||||
: new FileDataSourceFactory();
|
||||
DataSink.Factory writeDataSinkFactory =
|
||||
cacheWriteDataSinkFactory != null
|
||||
? cacheWriteDataSinkFactory
|
||||
: new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_MAX_CACHE_FILE_SIZE);
|
||||
if (cacheWriteDataSinkFactory == null) {
|
||||
cacheWriteDataSinkFactory =
|
||||
new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE);
|
||||
}
|
||||
onlineCacheDataSourceFactory =
|
||||
new CacheDataSourceFactory(
|
||||
cache,
|
||||
upstreamFactory,
|
||||
readDataSourceFactory,
|
||||
writeDataSinkFactory,
|
||||
cacheWriteDataSinkFactory,
|
||||
CacheDataSource.FLAG_BLOCK_ON_CACHE,
|
||||
/* eventListener= */ null,
|
||||
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) {
|
||||
this.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.dataSource = constructorHelper.createCacheDataSource();
|
||||
this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();
|
||||
|
@ -155,16 +155,14 @@ public final class Requirements {
|
||||
* @return Whether the requirements are met.
|
||||
*/
|
||||
public boolean checkRequirements(Context context) {
|
||||
return checkNetworkRequirements(context)
|
||||
&& checkChargingRequirement(context)
|
||||
&& checkIdleRequirement(context);
|
||||
return getNotMetRequirements(context) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requirement flags that are not met, or 0.
|
||||
* Returns {@link RequirementFlags} that are not met, or 0.
|
||||
*
|
||||
* @param context Any context.
|
||||
* @return The requirement flags that are not met, or 0.
|
||||
* @return RequirementFlags that are not met, or 0.
|
||||
*/
|
||||
@RequirementFlags
|
||||
public int getNotMetRequirements(Context context) {
|
||||
@ -202,7 +200,7 @@ public final class Requirements {
|
||||
logd("Roaming: " + roaming);
|
||||
return !roaming;
|
||||
}
|
||||
boolean activeNetworkMetered = isActiveNetworkMetered(connectivityManager, networkInfo);
|
||||
boolean activeNetworkMetered = connectivityManager.isActiveNetworkMetered();
|
||||
logd("Metered network: " + activeNetworkMetered);
|
||||
if (networkRequirement == NETWORK_TYPE_UNMETERED) {
|
||||
return !activeNetworkMetered;
|
||||
@ -257,17 +255,6 @@ public final class Requirements {
|
||||
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) {
|
||||
if (Scheduler.DEBUG) {
|
||||
Log.d(TAG, message);
|
||||
@ -285,4 +272,20 @@ public final class Requirements {
|
||||
+ (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.
|
||||
*/
|
||||
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 notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not
|
||||
* met, or 0.
|
||||
*/
|
||||
void requirementsMet(RequirementsWatcher requirementsWatcher);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
void onRequirementsStateChanged(
|
||||
RequirementsWatcher requirementsWatcher,
|
||||
@Requirements.RequirementFlags int notMetRequirements);
|
||||
}
|
||||
|
||||
private static final String TAG = "RequirementsWatcher";
|
||||
@ -66,8 +61,9 @@ public final class RequirementsWatcher {
|
||||
private final Requirements requirements;
|
||||
private DeviceStatusChangeReceiver receiver;
|
||||
|
||||
private int notMetRequirements;
|
||||
@Requirements.RequirementFlags private int notMetRequirements;
|
||||
private CapabilityValidatedCallback networkCallback;
|
||||
private Handler handler;
|
||||
|
||||
/**
|
||||
* @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
|
||||
* 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());
|
||||
handler = new Handler();
|
||||
|
||||
notMetRequirements = requirements.getNotMetRequirements(context);
|
||||
|
||||
@ -111,8 +111,9 @@ public final class RequirementsWatcher {
|
||||
}
|
||||
}
|
||||
receiver = new DeviceStatusChangeReceiver();
|
||||
context.registerReceiver(receiver, filter, null, new Handler());
|
||||
context.registerReceiver(receiver, filter, null, handler);
|
||||
logd(this + " started");
|
||||
return notMetRequirements;
|
||||
}
|
||||
|
||||
/** Stops watching for changes. */
|
||||
@ -160,18 +161,12 @@ public final class RequirementsWatcher {
|
||||
}
|
||||
|
||||
private void checkRequirements() {
|
||||
@Requirements.RequirementFlags
|
||||
int notMetRequirements = requirements.getNotMetRequirements(context);
|
||||
if (this.notMetRequirements == notMetRequirements) {
|
||||
logd("notMetRequirements hasn't changed: " + notMetRequirements);
|
||||
return;
|
||||
}
|
||||
this.notMetRequirements = notMetRequirements;
|
||||
if (notMetRequirements == 0) {
|
||||
logd("start job");
|
||||
listener.requirementsMet(this);
|
||||
} else {
|
||||
logd("stop job");
|
||||
listener.requirementsNotMet(this);
|
||||
if (this.notMetRequirements != notMetRequirements) {
|
||||
this.notMetRequirements = notMetRequirements;
|
||||
logd("notMetRequirements has changed: " + notMetRequirements);
|
||||
listener.onRequirementsStateChanged(this, notMetRequirements);
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,16 +190,22 @@ public final class RequirementsWatcher {
|
||||
private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback {
|
||||
@Override
|
||||
public void onAvailable(Network network) {
|
||||
super.onAvailable(network);
|
||||
logd(RequirementsWatcher.this + " NetworkCallback.onAvailable");
|
||||
checkRequirements();
|
||||
onNetworkCallback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLost(Network network) {
|
||||
super.onLost(network);
|
||||
logd(RequirementsWatcher.this + " NetworkCallback.onLost");
|
||||
checkRequirements();
|
||||
onNetworkCallback();
|
||||
}
|
||||
|
||||
private void onNetworkCallback() {
|
||||
handler.post(
|
||||
() -> {
|
||||
if (networkCallback != null) {
|
||||
logd(RequirementsWatcher.this + " NetworkCallback");
|
||||
checkRequirements();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -206,10 +206,10 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
ClippingMediaPeriod mediaPeriod =
|
||||
new ClippingMediaPeriod(
|
||||
mediaSource.createPeriod(id, allocator),
|
||||
mediaSource.createPeriod(id, allocator, startPositionUs),
|
||||
enableInitialDiscontinuity,
|
||||
periodStartUs,
|
||||
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.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.EventDispatcher;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@ -36,9 +35,11 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 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_MOVE = 2;
|
||||
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;
|
||||
|
||||
// Accessed on any thread.
|
||||
@GuardedBy("this")
|
||||
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.
|
||||
private final List<MediaSourceHolder> mediaSourceHolders;
|
||||
@ -67,8 +75,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
private final Timeline.Window window;
|
||||
private final Timeline.Period period;
|
||||
|
||||
private boolean listenerNotificationScheduled;
|
||||
private EventDispatcher<Runnable> pendingOnCompletionActions;
|
||||
private boolean timelineUpdateScheduled;
|
||||
private Set<HandlerAndRunnable> nextTimelineUpdateOnCompletionActions;
|
||||
private ShuffleOrder shuffleOrder;
|
||||
private int windowCount;
|
||||
private int periodCount;
|
||||
@ -127,7 +135,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
this.mediaSourceByUid = new HashMap<>();
|
||||
this.mediaSourcesPublic = new ArrayList<>();
|
||||
this.mediaSourceHolders = new ArrayList<>();
|
||||
this.pendingOnCompletionActions = new EventDispatcher<>();
|
||||
this.nextTimelineUpdateOnCompletionActions = new HashSet<>();
|
||||
this.pendingOnCompletionActions = new HashSet<>();
|
||||
this.isAtomic = isAtomic;
|
||||
this.useLazyPreparation = useLazyPreparation;
|
||||
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.
|
||||
*
|
||||
* @param mediaSource The {@link MediaSource} to be added to the list.
|
||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
||||
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||
* source has been added to the playlist.
|
||||
*/
|
||||
public final synchronized void addMediaSource(
|
||||
MediaSource mediaSource, Handler handler, Runnable actionOnCompletion) {
|
||||
addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, actionOnCompletion);
|
||||
MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
|
||||
addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, onCompletionAction);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -169,7 +178,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
index,
|
||||
Collections.singletonList(mediaSource),
|
||||
/* 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
|
||||
* be in the range of 0 <= index <= {@link #getSize()}.
|
||||
* @param mediaSource The {@link MediaSource} to be added to the list.
|
||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
||||
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||
* source has been added to the playlist.
|
||||
*/
|
||||
public final synchronized void addMediaSource(
|
||||
int index, MediaSource mediaSource, Handler handler, Runnable actionOnCompletion) {
|
||||
int index, MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
|
||||
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(),
|
||||
mediaSources,
|
||||
/* 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
|
||||
* sources are added in the order in which they appear in this collection.
|
||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
||||
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||
* sources have been added to the playlist.
|
||||
*/
|
||||
public final synchronized void addMediaSources(
|
||||
Collection<MediaSource> mediaSources, Handler handler, Runnable actionOnCompletion) {
|
||||
addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, actionOnCompletion);
|
||||
Collection<MediaSource> mediaSources, Handler handler, Runnable onCompletionAction) {
|
||||
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.
|
||||
*/
|
||||
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()}.
|
||||
* @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.
|
||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
||||
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||
* sources have been added to the playlist.
|
||||
*/
|
||||
public final synchronized void addMediaSources(
|
||||
int index,
|
||||
Collection<MediaSource> mediaSources,
|
||||
Handler handler,
|
||||
Runnable actionOnCompletion) {
|
||||
addPublicMediaSources(index, mediaSources, handler, actionOnCompletion);
|
||||
Runnable onCompletionAction) {
|
||||
addPublicMediaSources(index, mediaSources, handler, onCompletionAction);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -261,7 +270,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
*/
|
||||
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
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
||||
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||
* source has been removed from the playlist.
|
||||
*/
|
||||
public final synchronized void removeMediaSource(
|
||||
int index, Handler handler, Runnable actionOnCompletion) {
|
||||
removePublicMediaSources(index, index + 1, handler, actionOnCompletion);
|
||||
int index, Handler handler, Runnable onCompletionAction) {
|
||||
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) {
|
||||
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()}.
|
||||
* @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()}.
|
||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
||||
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||
* source range has been removed from the playlist.
|
||||
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
|
||||
* {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex}
|
||||
*/
|
||||
public final synchronized void removeMediaSourceRange(
|
||||
int fromIndex, int toIndex, Handler handler, Runnable actionOnCompletion) {
|
||||
removePublicMediaSources(fromIndex, toIndex, handler, actionOnCompletion);
|
||||
int fromIndex, int toIndex, Handler handler, Runnable onCompletionAction) {
|
||||
removePublicMediaSources(fromIndex, toIndex, handler, onCompletionAction);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -335,7 +344,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
*/
|
||||
public final synchronized void moveMediaSource(int currentIndex, int newIndex) {
|
||||
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()}.
|
||||
* @param newIndex The target index of the media source in the playlist. This index must be in the
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
|
||||
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||
* @param onCompletionAction A {@link Runnable} which is executed immediately after the media
|
||||
* source has been moved.
|
||||
*/
|
||||
public final synchronized void moveMediaSource(
|
||||
int currentIndex, int newIndex, Handler handler, Runnable actionOnCompletion) {
|
||||
movePublicMediaSource(currentIndex, newIndex, handler, actionOnCompletion);
|
||||
int currentIndex, int newIndex, Handler handler, Runnable onCompletionAction) {
|
||||
movePublicMediaSource(currentIndex, newIndex, handler, onCompletionAction);
|
||||
}
|
||||
|
||||
/** Clears the playlist. */
|
||||
@ -363,12 +372,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
/**
|
||||
* Clears the playlist and executes a custom action on completion.
|
||||
*
|
||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the playlist
|
||||
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||
* @param onCompletionAction A {@link Runnable} which is executed immediately after the playlist
|
||||
* has been cleared.
|
||||
*/
|
||||
public final synchronized void clear(Handler handler, Runnable actionOnCompletion) {
|
||||
removeMediaSourceRange(0, getSize(), handler, actionOnCompletion);
|
||||
public final synchronized void clear(Handler handler, Runnable onCompletionAction) {
|
||||
removeMediaSourceRange(0, getSize(), handler, onCompletionAction);
|
||||
}
|
||||
|
||||
/** Returns the number of media sources in the playlist. */
|
||||
@ -392,20 +401,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
* @param shuffleOrder A {@link 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.
|
||||
*
|
||||
* @param shuffleOrder A {@link ShuffleOrder}.
|
||||
* @param handler The {@link Handler} to run {@code actionOnCompletion}.
|
||||
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the shuffle
|
||||
* @param handler The {@link Handler} to run {@code onCompletionAction}.
|
||||
* @param onCompletionAction A {@link Runnable} which is executed immediately after the shuffle
|
||||
* order has been changed.
|
||||
*/
|
||||
public final synchronized void setShuffleOrder(
|
||||
ShuffleOrder shuffleOrder, Handler handler, Runnable actionOnCompletion) {
|
||||
setPublicShuffleOrder(shuffleOrder, handler, actionOnCompletion);
|
||||
ShuffleOrder shuffleOrder, Handler handler, Runnable onCompletionAction) {
|
||||
setPublicShuffleOrder(shuffleOrder, handler, onCompletionAction);
|
||||
}
|
||||
|
||||
// CompositeMediaSource implementation.
|
||||
@ -422,11 +431,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
super.prepareSourceInternal(mediaTransferListener);
|
||||
playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);
|
||||
if (mediaSourcesPublic.isEmpty()) {
|
||||
notifyListener();
|
||||
updateTimelineAndScheduleOnCompletionActions();
|
||||
} else {
|
||||
shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());
|
||||
addMediaSourcesInternal(0, mediaSourcesPublic);
|
||||
scheduleListenerNotification();
|
||||
scheduleTimelineUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@ -438,7 +447,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
}
|
||||
|
||||
@Override
|
||||
public final MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public final MediaPeriod createPeriod(
|
||||
MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
|
||||
MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid);
|
||||
if (holder == null) {
|
||||
@ -446,7 +456,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
holder = new MediaSourceHolder(new DummyMediaSource());
|
||||
holder.hasStartedPreparing = true;
|
||||
}
|
||||
DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, id, allocator);
|
||||
DeferredMediaPeriod mediaPeriod =
|
||||
new DeferredMediaPeriod(holder.mediaSource, id, allocator, startPositionUs);
|
||||
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
|
||||
holder.activeMediaPeriods.add(mediaPeriod);
|
||||
if (!holder.hasStartedPreparing) {
|
||||
@ -473,10 +484,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
super.releaseSourceInternal();
|
||||
mediaSourceHolders.clear();
|
||||
mediaSourceByUid.clear();
|
||||
playbackThreadHandler = null;
|
||||
shuffleOrder = shuffleOrder.cloneAndClear();
|
||||
windowCount = 0;
|
||||
periodCount = 0;
|
||||
if (playbackThreadHandler != null) {
|
||||
playbackThreadHandler.removeCallbacksAndMessages(null);
|
||||
playbackThreadHandler = null;
|
||||
}
|
||||
timelineUpdateScheduled = false;
|
||||
nextTimelineUpdateOnCompletionActions.clear();
|
||||
dispatchOnCompletionActions(pendingOnCompletionActions);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -516,8 +533,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
int index,
|
||||
Collection<MediaSource> mediaSources,
|
||||
@Nullable Handler handler,
|
||||
@Nullable Runnable actionOnCompletion) {
|
||||
Assertions.checkArgument((handler == null) == (actionOnCompletion == null));
|
||||
@Nullable Runnable onCompletionAction) {
|
||||
Assertions.checkArgument((handler == null) == (onCompletionAction == null));
|
||||
Handler playbackThreadHandler = this.playbackThreadHandler;
|
||||
for (MediaSource mediaSource : mediaSources) {
|
||||
Assertions.checkNotNull(mediaSource);
|
||||
}
|
||||
@ -527,12 +545,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
}
|
||||
mediaSourcesPublic.addAll(index, mediaSourceHolders);
|
||||
if (playbackThreadHandler != null && !mediaSources.isEmpty()) {
|
||||
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
|
||||
playbackThreadHandler
|
||||
.obtainMessage(
|
||||
MSG_ADD, new MessageData<>(index, mediaSourceHolders, handler, actionOnCompletion))
|
||||
.obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, callbackAction))
|
||||
.sendToTarget();
|
||||
} else if (actionOnCompletion != null && handler != null) {
|
||||
handler.post(actionOnCompletion);
|
||||
} else if (onCompletionAction != null && handler != null) {
|
||||
handler.post(onCompletionAction);
|
||||
}
|
||||
}
|
||||
|
||||
@ -541,16 +559,17 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
int fromIndex,
|
||||
int toIndex,
|
||||
@Nullable Handler handler,
|
||||
@Nullable Runnable actionOnCompletion) {
|
||||
Assertions.checkArgument((handler == null) == (actionOnCompletion == null));
|
||||
@Nullable Runnable onCompletionAction) {
|
||||
Assertions.checkArgument((handler == null) == (onCompletionAction == null));
|
||||
Handler playbackThreadHandler = this.playbackThreadHandler;
|
||||
Util.removeRange(mediaSourcesPublic, fromIndex, toIndex);
|
||||
if (playbackThreadHandler != null) {
|
||||
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
|
||||
playbackThreadHandler
|
||||
.obtainMessage(
|
||||
MSG_REMOVE, new MessageData<>(fromIndex, toIndex, handler, actionOnCompletion))
|
||||
.obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, callbackAction))
|
||||
.sendToTarget();
|
||||
} else if (actionOnCompletion != null && handler != null) {
|
||||
handler.post(actionOnCompletion);
|
||||
} else if (onCompletionAction != null && handler != null) {
|
||||
handler.post(onCompletionAction);
|
||||
}
|
||||
}
|
||||
|
||||
@ -559,23 +578,24 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
int currentIndex,
|
||||
int newIndex,
|
||||
@Nullable Handler handler,
|
||||
@Nullable Runnable actionOnCompletion) {
|
||||
Assertions.checkArgument((handler == null) == (actionOnCompletion == null));
|
||||
@Nullable Runnable onCompletionAction) {
|
||||
Assertions.checkArgument((handler == null) == (onCompletionAction == null));
|
||||
Handler playbackThreadHandler = this.playbackThreadHandler;
|
||||
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
|
||||
if (playbackThreadHandler != null) {
|
||||
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
|
||||
playbackThreadHandler
|
||||
.obtainMessage(
|
||||
MSG_MOVE, new MessageData<>(currentIndex, newIndex, handler, actionOnCompletion))
|
||||
.obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, callbackAction))
|
||||
.sendToTarget();
|
||||
} else if (actionOnCompletion != null && handler != null) {
|
||||
handler.post(actionOnCompletion);
|
||||
} else if (onCompletionAction != null && handler != null) {
|
||||
handler.post(onCompletionAction);
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("this")
|
||||
private void setPublicShuffleOrder(
|
||||
ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable actionOnCompletion) {
|
||||
Assertions.checkArgument((handler == null) == (actionOnCompletion == null));
|
||||
ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable onCompletionAction) {
|
||||
Assertions.checkArgument((handler == null) == (onCompletionAction == null));
|
||||
Handler playbackThreadHandler = this.playbackThreadHandler;
|
||||
if (playbackThreadHandler != null) {
|
||||
int size = getSize();
|
||||
@ -585,35 +605,44 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
.cloneAndClear()
|
||||
.cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
|
||||
}
|
||||
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
|
||||
playbackThreadHandler
|
||||
.obtainMessage(
|
||||
MSG_SET_SHUFFLE_ORDER,
|
||||
new MessageData<>(/* index= */ 0, shuffleOrder, handler, actionOnCompletion))
|
||||
new MessageData<>(/* index= */ 0, shuffleOrder, callbackAction))
|
||||
.sendToTarget();
|
||||
} else {
|
||||
this.shuffleOrder =
|
||||
shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
|
||||
if (actionOnCompletion != null && handler != null) {
|
||||
handler.post(actionOnCompletion);
|
||||
if (onCompletionAction != null && handler != null) {
|
||||
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.
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean handleMessage(Message msg) {
|
||||
if (playbackThreadHandler == null) {
|
||||
// Stale event.
|
||||
return false;
|
||||
}
|
||||
switch (msg.what) {
|
||||
case MSG_ADD:
|
||||
MessageData<Collection<MediaSourceHolder>> addMessage =
|
||||
(MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj);
|
||||
shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size());
|
||||
addMediaSourcesInternal(addMessage.index, addMessage.customData);
|
||||
scheduleListenerNotification(addMessage.handler, addMessage.actionOnCompletion);
|
||||
scheduleTimelineUpdate(addMessage.onCompletionAction);
|
||||
break;
|
||||
case MSG_REMOVE:
|
||||
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--) {
|
||||
removeMediaSourceInternal(index);
|
||||
}
|
||||
scheduleListenerNotification(removeMessage.handler, removeMessage.actionOnCompletion);
|
||||
scheduleTimelineUpdate(removeMessage.onCompletionAction);
|
||||
break;
|
||||
case MSG_MOVE:
|
||||
MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
|
||||
shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1);
|
||||
shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);
|
||||
moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
|
||||
scheduleListenerNotification(moveMessage.handler, moveMessage.actionOnCompletion);
|
||||
scheduleTimelineUpdate(moveMessage.onCompletionAction);
|
||||
break;
|
||||
case MSG_SET_SHUFFLE_ORDER:
|
||||
MessageData<ShuffleOrder> shuffleOrderMessage =
|
||||
(MessageData<ShuffleOrder>) Util.castNonNull(msg.obj);
|
||||
shuffleOrder = shuffleOrderMessage.customData;
|
||||
scheduleListenerNotification(
|
||||
shuffleOrderMessage.handler, shuffleOrderMessage.actionOnCompletion);
|
||||
scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction);
|
||||
break;
|
||||
case MSG_NOTIFY_LISTENER:
|
||||
notifyListener();
|
||||
case MSG_UPDATE_TIMELINE:
|
||||
updateTimelineAndScheduleOnCompletionActions();
|
||||
break;
|
||||
case MSG_ON_COMPLETION:
|
||||
EventDispatcher<Runnable> actionsOnCompletion =
|
||||
(EventDispatcher<Runnable>) Util.castNonNull(msg.obj);
|
||||
actionsOnCompletion.dispatch(Runnable::run);
|
||||
Set<HandlerAndRunnable> actions = (Set<HandlerAndRunnable>) Util.castNonNull(msg.obj);
|
||||
dispatchOnCompletionActions(actions);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
@ -657,36 +684,48 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
return true;
|
||||
}
|
||||
|
||||
private void scheduleListenerNotification() {
|
||||
scheduleListenerNotification(/* handler= */ null, /* actionOnCompletion= */ null);
|
||||
private void scheduleTimelineUpdate() {
|
||||
scheduleTimelineUpdate(/* onCompletionAction= */ null);
|
||||
}
|
||||
|
||||
private void scheduleListenerNotification(
|
||||
@Nullable Handler handler, @Nullable Runnable actionOnCompletion) {
|
||||
if (!listenerNotificationScheduled) {
|
||||
Assertions.checkNotNull(playbackThreadHandler)
|
||||
.obtainMessage(MSG_NOTIFY_LISTENER)
|
||||
.sendToTarget();
|
||||
listenerNotificationScheduled = true;
|
||||
private void scheduleTimelineUpdate(@Nullable HandlerAndRunnable onCompletionAction) {
|
||||
if (!timelineUpdateScheduled) {
|
||||
getPlaybackThreadHandlerOnPlaybackThread().obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget();
|
||||
timelineUpdateScheduled = true;
|
||||
}
|
||||
if (actionOnCompletion != null && handler != null) {
|
||||
pendingOnCompletionActions.addListener(handler, actionOnCompletion);
|
||||
if (onCompletionAction != null) {
|
||||
nextTimelineUpdateOnCompletionActions.add(onCompletionAction);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyListener() {
|
||||
listenerNotificationScheduled = false;
|
||||
EventDispatcher<Runnable> actionsOnCompletion = pendingOnCompletionActions;
|
||||
pendingOnCompletionActions = new EventDispatcher<>();
|
||||
private void updateTimelineAndScheduleOnCompletionActions() {
|
||||
timelineUpdateScheduled = false;
|
||||
Set<HandlerAndRunnable> onCompletionActions = nextTimelineUpdateOnCompletionActions;
|
||||
nextTimelineUpdateOnCompletionActions = new HashSet<>();
|
||||
refreshSourceInfo(
|
||||
new ConcatenatedTimeline(
|
||||
mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
|
||||
/* manifest= */ null);
|
||||
Assertions.checkNotNull(playbackThreadHandler)
|
||||
.obtainMessage(MSG_ON_COMPLETION, actionsOnCompletion)
|
||||
getPlaybackThreadHandlerOnPlaybackThread()
|
||||
.obtainMessage(MSG_ON_COMPLETION, onCompletionActions)
|
||||
.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(
|
||||
int index, Collection<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
|
||||
// playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions
|
||||
// anyway.
|
||||
timeline.getWindow(/* windowIndex= */ 0, window);
|
||||
long windowStartPositionUs = window.getDefaultPositionUs();
|
||||
if (deferredMediaPeriod != null) {
|
||||
long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs();
|
||||
@ -782,7 +822,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
}
|
||||
}
|
||||
mediaSourceHolder.isPrepared = true;
|
||||
scheduleListenerNotification();
|
||||
scheduleTimelineUpdate();
|
||||
}
|
||||
|
||||
private void removeMediaSourceInternal(int index) {
|
||||
@ -895,15 +935,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
|
||||
public final int index;
|
||||
public final T customData;
|
||||
@Nullable public final Handler handler;
|
||||
@Nullable public final Runnable actionOnCompletion;
|
||||
@Nullable public final HandlerAndRunnable onCompletionAction;
|
||||
|
||||
public MessageData(
|
||||
int index, T customData, @Nullable Handler handler, @Nullable Runnable actionOnCompletion) {
|
||||
public MessageData(int index, T customData, @Nullable HandlerAndRunnable onCompletionAction) {
|
||||
this.index = index;
|
||||
this.customData = customData;
|
||||
this.handler = handler;
|
||||
this.actionOnCompletion = actionOnCompletion;
|
||||
this.onCompletionAction = onCompletionAction;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1144,7 +1181,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@ -1153,5 +1190,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||
// 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
|
||||
* 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
|
||||
* 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 id The identifier used to create the deferred 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.allocator = allocator;
|
||||
this.mediaSource = mediaSource;
|
||||
this.preparePositionUs = preparePositionUs;
|
||||
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
|
||||
* 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) {
|
||||
preparePositionOverrideUs = defaultPreparePositionUs;
|
||||
public void overridePreparePositionUs(long preparePositionUs) {
|
||||
preparePositionOverrideUs = preparePositionUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then
|
||||
* prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()}
|
||||
* to release the period.
|
||||
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source
|
||||
* then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link
|
||||
* #releasePeriod()} to release the period.
|
||||
*
|
||||
* @param id The identifier that should be used to create the media period from the media source.
|
||||
*/
|
||||
public void createPeriod(MediaPeriodId id) {
|
||||
mediaPeriod = mediaSource.createPeriod(id, allocator);
|
||||
long preparePositionUs = getPreparePositionWithOverride(this.preparePositionUs);
|
||||
mediaPeriod = mediaSource.createPeriod(id, allocator, preparePositionUs);
|
||||
if (callback != null) {
|
||||
long preparePositionUs =
|
||||
preparePositionOverrideUs != C.TIME_UNSET
|
||||
? preparePositionOverrideUs
|
||||
: this.preparePositionUs;
|
||||
mediaPeriod.prepare(this, preparePositionUs);
|
||||
}
|
||||
}
|
||||
@ -124,9 +124,8 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
||||
@Override
|
||||
public void prepare(Callback callback, long preparePositionUs) {
|
||||
this.callback = callback;
|
||||
this.preparePositionUs = preparePositionUs;
|
||||
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);
|
||||
}
|
||||
|
||||
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 com.google.android.exoplayer2.C;
|
||||
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.Extractor;
|
||||
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 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 extractors for AAC, MPEG PS/TS and FLV streams do not support seeking.
|
||||
*/
|
||||
/** @deprecated Use {@link ProgressiveMediaSource} instead. */
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
public final class ExtractorMediaSource extends BaseMediaSource
|
||||
implements ExtractorMediaPeriod.Listener {
|
||||
implements MediaSource.SourceInfoRefreshListener {
|
||||
|
||||
/**
|
||||
* Listener of {@link ExtractorMediaSource} events.
|
||||
*
|
||||
* @deprecated Use {@link MediaSourceEventListener}.
|
||||
*/
|
||||
/** @deprecated Use {@link MediaSourceEventListener} instead. */
|
||||
@Deprecated
|
||||
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 {
|
||||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
@ -232,23 +222,11 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@Deprecated
|
||||
public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES =
|
||||
ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
|
||||
|
||||
private final Uri uri;
|
||||
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;
|
||||
private final ProgressiveMediaSource progressiveMediaSource;
|
||||
|
||||
/**
|
||||
* @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
|
||||
@SuppressWarnings("deprecation")
|
||||
public ExtractorMediaSource(
|
||||
Uri uri,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
@ -284,7 +261,6 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
||||
* @deprecated Use {@link Factory} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
public ExtractorMediaSource(
|
||||
Uri uri,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
@ -317,7 +293,6 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
||||
* @deprecated Use {@link Factory} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("deprecation")
|
||||
public ExtractorMediaSource(
|
||||
Uri uri,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
@ -347,93 +322,57 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
||||
@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;
|
||||
progressiveMediaSource =
|
||||
new ProgressiveMediaSource(
|
||||
uri,
|
||||
dataSourceFactory,
|
||||
extractorsFactory,
|
||||
loadableLoadErrorHandlingPolicy,
|
||||
customCacheKey,
|
||||
continueLoadingCheckIntervalBytes,
|
||||
tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object getTag() {
|
||||
return tag;
|
||||
return progressiveMediaSource.getTag();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||
transferListener = mediaTransferListener;
|
||||
notifySourceInfoRefreshed(timelineDurationUs, /* isSeekable= */ false);
|
||||
progressiveMediaSource.prepareSource(/* listener= */ this, mediaTransferListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||
// Do nothing.
|
||||
progressiveMediaSource.maybeThrowSourceInfoRefreshError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
DataSource dataSource = dataSourceFactory.createDataSource();
|
||||
if (transferListener != null) {
|
||||
dataSource.addTransferListener(transferListener);
|
||||
}
|
||||
return new ExtractorMediaPeriod(
|
||||
uri,
|
||||
dataSource,
|
||||
extractorsFactory.createExtractors(),
|
||||
loadableLoadErrorHandlingPolicy,
|
||||
createEventDispatcher(id),
|
||||
this,
|
||||
allocator,
|
||||
customCacheKey,
|
||||
continueLoadingCheckIntervalBytes);
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
return progressiveMediaSource.createPeriod(id, allocator, startPositionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||
((ExtractorMediaPeriod) mediaPeriod).release();
|
||||
progressiveMediaSource.releasePeriod(mediaPeriod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseSourceInternal() {
|
||||
// Do nothing.
|
||||
progressiveMediaSource.releaseSource(/* listener= */ this);
|
||||
}
|
||||
|
||||
// ExtractorMediaPeriod.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);
|
||||
public void onSourceInfoRefreshed(
|
||||
MediaSource source, Timeline timeline, @Nullable Object manifest) {
|
||||
refreshSourceInfo(timeline, manifest);
|
||||
}
|
||||
|
||||
// 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
|
||||
@SuppressWarnings("deprecation")
|
||||
private static final class EventListenerWrapper extends DefaultMediaSourceEventListener {
|
||||
|
||||
private final EventListener eventListener;
|
||||
|
||||
public EventListenerWrapper(EventListener eventListener) {
|
||||
|
@ -31,7 +31,7 @@ import java.util.Map;
|
||||
* Loops a {@link MediaSource} a specified number of times.
|
||||
*
|
||||
* <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> {
|
||||
|
||||
@ -77,14 +77,15 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
if (loopCount == Integer.MAX_VALUE) {
|
||||
return childSource.createPeriod(id, allocator);
|
||||
return childSource.createPeriod(id, allocator, startPositionUs);
|
||||
}
|
||||
Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid);
|
||||
MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid);
|
||||
childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id);
|
||||
MediaPeriod mediaPeriod = childSource.createPeriod(childMediaPeriodId, allocator);
|
||||
MediaPeriod mediaPeriod =
|
||||
childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
|
||||
mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId);
|
||||
return mediaPeriod;
|
||||
}
|
||||
|
@ -87,18 +87,18 @@ public interface MediaPeriod extends SequenceableLoader {
|
||||
TrackGroupArray getTrackGroups();
|
||||
|
||||
/**
|
||||
* Returns a list of {@link StreamKey stream keys} which allow to filter the media in this period
|
||||
* to load only the parts needed to play the provided {@link TrackSelection}.
|
||||
* 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 TrackSelections}.
|
||||
*
|
||||
* <p>This method is only called after the period has been prepared.
|
||||
*
|
||||
* @param trackSelection The {@link TrackSelection} describing the tracks for which stream keys
|
||||
* are requested.
|
||||
* @return The corresponding {@link StreamKey stream keys} for the selected tracks, or an empty
|
||||
* @param trackSelections The {@link TrackSelection TrackSelections} describing the tracks for
|
||||
* which stream keys are requested.
|
||||
* @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
|
||||
* selected tracks.
|
||||
*/
|
||||
default List<StreamKey> getStreamKeys(TrackSelection trackSelection) {
|
||||
default List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
@ -34,8 +35,8 @@ import java.io.IOException;
|
||||
* on the {@link SourceInfoRefreshListener}s passed to {@link
|
||||
* #prepareSource(SourceInfoRefreshListener, TransferListener)}.
|
||||
* <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
|
||||
* the player to load and read the media.
|
||||
* obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a
|
||||
* way for the player to load and read the media.
|
||||
* </ul>
|
||||
*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* The index of the next ad group to which the media period's content is clipped, or {@link
|
||||
* C#INDEX_UNSET} if there is no following ad group or if this media period is an ad.
|
||||
*/
|
||||
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
|
||||
@ -103,7 +102,7 @@ public interface MediaSource {
|
||||
* @param periodUid The unique id of the timeline period.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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 windowSequenceNumber The sequence number of the window in the buffered sequence of
|
||||
* windows this media period is part of.
|
||||
* @param endPositionUs The end position of the media period within the timeline period, in
|
||||
* microseconds.
|
||||
* @param nextAdGroupIndex The index of the next ad group to which the media period's content is
|
||||
* clipped.
|
||||
*/
|
||||
public MediaPeriodId(Object periodUid, long windowSequenceNumber, long endPositionUs) {
|
||||
this(periodUid, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, endPositionUs);
|
||||
public MediaPeriodId(Object periodUid, long windowSequenceNumber, int nextAdGroupIndex) {
|
||||
this(
|
||||
periodUid,
|
||||
/* adGroupIndex= */ C.INDEX_UNSET,
|
||||
/* adIndexInAdGroup= */ C.INDEX_UNSET,
|
||||
windowSequenceNumber,
|
||||
nextAdGroupIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -142,7 +151,12 @@ public interface MediaSource {
|
||||
*/
|
||||
public MediaPeriodId(
|
||||
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(
|
||||
@ -150,12 +164,12 @@ public interface MediaSource {
|
||||
int adGroupIndex,
|
||||
int adIndexInAdGroup,
|
||||
long windowSequenceNumber,
|
||||
long endPositionUs) {
|
||||
int nextAdGroupIndex) {
|
||||
this.periodUid = periodUid;
|
||||
this.adGroupIndex = adGroupIndex;
|
||||
this.adIndexInAdGroup = adIndexInAdGroup;
|
||||
this.windowSequenceNumber = windowSequenceNumber;
|
||||
this.endPositionUs = endPositionUs;
|
||||
this.nextAdGroupIndex = nextAdGroupIndex;
|
||||
}
|
||||
|
||||
/** 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)
|
||||
? this
|
||||
: new MediaPeriodId(
|
||||
newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, endPositionUs);
|
||||
newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, nextAdGroupIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -187,7 +201,7 @@ public interface MediaSource {
|
||||
&& adGroupIndex == periodId.adGroupIndex
|
||||
&& adIndexInAdGroup == periodId.adIndexInAdGroup
|
||||
&& windowSequenceNumber == periodId.windowSequenceNumber
|
||||
&& endPositionUs == periodId.endPositionUs;
|
||||
&& nextAdGroupIndex == periodId.nextAdGroupIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -197,7 +211,7 @@ public interface MediaSource {
|
||||
result = 31 * result + adGroupIndex;
|
||||
result = 31 * result + adIndexInAdGroup;
|
||||
result = 31 * result + (int) windowSequenceNumber;
|
||||
result = 31 * result + (int) endPositionUs;
|
||||
result = 31 * result + nextAdGroupIndex;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -224,7 +238,6 @@ public interface MediaSource {
|
||||
default Object getTag() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts source preparation if not yet started, and adds a listener for timeline and/or manifest
|
||||
* updates.
|
||||
@ -243,8 +256,7 @@ public interface MediaSource {
|
||||
* and other data.
|
||||
*/
|
||||
void prepareSource(
|
||||
SourceInfoRefreshListener listener,
|
||||
@Nullable TransferListener mediaTransferListener);
|
||||
SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener);
|
||||
|
||||
/**
|
||||
* 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 allocator An {@link Allocator} from which to obtain media buffer allocations.
|
||||
* @param startPositionUs The expected start position, in microseconds.
|
||||
* @return A new {@link MediaPeriod}.
|
||||
*/
|
||||
MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator);
|
||||
MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs);
|
||||
|
||||
/**
|
||||
* Releases the period.
|
||||
|
@ -120,13 +120,13 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
|
||||
int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
|
||||
for (int i = 0; i < periods.length; i++) {
|
||||
MediaPeriodId childMediaPeriodId =
|
||||
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);
|
||||
}
|
||||
|
@ -54,12 +54,13 @@ import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
|
||||
/**
|
||||
* A {@link MediaPeriod} that extracts data using an {@link Extractor}.
|
||||
*/
|
||||
/* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput,
|
||||
Loader.Callback<ExtractorMediaPeriod.ExtractingLoadable>, Loader.ReleaseCallback,
|
||||
UpstreamFormatChangedListener {
|
||||
/** A {@link MediaPeriod} that extracts data using an {@link Extractor}. */
|
||||
/* package */ final class ProgressiveMediaPeriod
|
||||
implements MediaPeriod,
|
||||
ExtractorOutput,
|
||||
Loader.Callback<ProgressiveMediaPeriod.ExtractingLoadable>,
|
||||
Loader.ReleaseCallback,
|
||||
UpstreamFormatChangedListener {
|
||||
|
||||
/**
|
||||
* Listener for information about the period.
|
||||
@ -145,7 +146,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
"nullness:argument.type.incompatible",
|
||||
"nullness:methodref.receiver.bound.invalid"
|
||||
})
|
||||
public ExtractorMediaPeriod(
|
||||
public ProgressiveMediaPeriod(
|
||||
Uri uri,
|
||||
DataSource dataSource,
|
||||
Extractor[] extractors,
|
||||
@ -163,14 +164,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
this.allocator = allocator;
|
||||
this.customCacheKey = customCacheKey;
|
||||
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
||||
loader = new Loader("Loader:ExtractorMediaPeriod");
|
||||
loader = new Loader("Loader:ProgressiveMediaPeriod");
|
||||
extractorHolder = new ExtractorHolder(extractors);
|
||||
loadCondition = new ConditionVariable();
|
||||
maybeFinishPrepareRunnable = this::maybeFinishPrepare;
|
||||
onContinueLoadingRequestedRunnable =
|
||||
() -> {
|
||||
if (!released) {
|
||||
Assertions.checkNotNull(callback).onContinueLoadingRequested(ExtractorMediaPeriod.this);
|
||||
Assertions.checkNotNull(callback)
|
||||
.onContinueLoadingRequested(ProgressiveMediaPeriod.this);
|
||||
}
|
||||
};
|
||||
handler = new Handler();
|
||||
@ -356,18 +358,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
} else if (isPendingReset()) {
|
||||
return pendingResetPositionUs;
|
||||
}
|
||||
long largestQueuedTimestampUs;
|
||||
long largestQueuedTimestampUs = Long.MAX_VALUE;
|
||||
if (haveAudioVideoTracks) {
|
||||
// Ignore non-AV tracks, which may be sparse or poorly interleaved.
|
||||
largestQueuedTimestampUs = Long.MAX_VALUE;
|
||||
int trackCount = sampleQueues.length;
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
if (trackIsAudioVideoFlags[i]) {
|
||||
if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) {
|
||||
largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
|
||||
sampleQueues[i].getLargestQueuedTimestampUs());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
if (largestQueuedTimestampUs == Long.MAX_VALUE) {
|
||||
largestQueuedTimestampUs = getLargestQueuedTimestampUs();
|
||||
}
|
||||
return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs
|
||||
@ -851,23 +853,23 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return ExtractorMediaPeriod.this.isReady(track);
|
||||
return ProgressiveMediaPeriod.this.isReady(track);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowError() throws IOException {
|
||||
ExtractorMediaPeriod.this.maybeThrowError();
|
||||
ProgressiveMediaPeriod.this.maybeThrowError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
|
||||
boolean formatRequired) {
|
||||
return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired);
|
||||
return ProgressiveMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired);
|
||||
}
|
||||
|
||||
@Override
|
||||
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,
|
||||
C.LENGTH_UNSET,
|
||||
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) {
|
@ -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 largestQueuedTimestampUs;
|
||||
private boolean isLastSampleQueued;
|
||||
private boolean upstreamKeyframeRequired;
|
||||
private boolean upstreamFormatRequired;
|
||||
private Format upstreamFormat;
|
||||
@ -93,6 +94,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
upstreamKeyframeRequired = true;
|
||||
largestDiscardedTimestampUs = Long.MIN_VALUE;
|
||||
largestQueuedTimestampUs = Long.MIN_VALUE;
|
||||
isLastSampleQueued = false;
|
||||
if (resetUpstreamFormat) {
|
||||
upstreamFormat = null;
|
||||
upstreamFormatRequired = true;
|
||||
@ -118,6 +120,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition));
|
||||
length -= discardCount;
|
||||
largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length));
|
||||
isLastSampleQueued = discardCount == 0 && isLastSampleQueued;
|
||||
if (length == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
@ -186,6 +189,19 @@ import com.google.android.exoplayer2.util.Util;
|
||||
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. */
|
||||
public synchronized long getFirstTimestampUs() {
|
||||
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,
|
||||
SampleExtrasHolder extrasHolder) {
|
||||
if (!hasNextSample()) {
|
||||
if (loadingFinished) {
|
||||
if (loadingFinished || isLastSampleQueued) {
|
||||
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
return C.RESULT_BUFFER_READ;
|
||||
} else if (upstreamFormat != null
|
||||
@ -388,7 +404,9 @@ import com.google.android.exoplayer2.util.Util;
|
||||
upstreamKeyframeRequired = false;
|
||||
}
|
||||
Assertions.checkState(!upstreamFormatRequired);
|
||||
commitSampleTimestamp(timeUs);
|
||||
|
||||
isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0;
|
||||
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
|
||||
|
||||
int relativeEndIndex = getRelativeIndex(length);
|
||||
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
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
public long 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