Merge branch 'dev-v2' into dev-v2

This commit is contained in:
zsmatyas 2019-02-07 13:13:47 -08:00 committed by GitHub
commit fd4998bcca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
194 changed files with 8311 additions and 3202 deletions

9
.gitignore vendored
View File

@ -37,6 +37,12 @@ local.properties
proguard.cfg proguard.cfg
proguard-project.txt proguard-project.txt
# Bazel
bazel-bin
bazel-genfiles
bazel-out
bazel-testlogs
# Other # Other
.DS_Store .DS_Store
cmake-build-debug cmake-build-debug
@ -66,3 +72,6 @@ extensions/cronet/jniLibs/*
extensions/cronet/libs/* extensions/cronet/libs/*
!extensions/cronet/libs/README.md !extensions/cronet/libs/README.md
# Cast receiver
cast_receiver_app/external-js
cast_receiver_app/bazel-cast_receiver_app

View File

@ -44,6 +44,12 @@ local.properties
proguard.cfg proguard.cfg
proguard-project.txt proguard-project.txt
# Bazel
bazel-bin
bazel-genfiles
bazel-out
bazel-testlogs
# Other # Other
.DS_Store .DS_Store
cmake-build-debug cmake-build-debug
@ -69,3 +75,7 @@ extensions/cronet/jniLibs/*
!extensions/cronet/jniLibs/README.md !extensions/cronet/jniLibs/README.md
extensions/cronet/libs/* extensions/cronet/libs/*
!extensions/cronet/libs/README.md !extensions/cronet/libs/README.md
# Cast receiver
cast_receiver_app/external-js
cast_receiver_app/bazel-cast_receiver_app

View File

@ -27,6 +27,8 @@ repository and depend on the modules locally.
### From JCenter ### ### From JCenter ###
#### 1. Add repositories ####
The easiest way to get started using ExoPlayer is to add it as a gradle The easiest way to get started using ExoPlayer is to add it as a gradle
dependency. You need to make sure you have the Google and JCenter repositories dependency. You need to make sure you have the Google and JCenter repositories
included in the `build.gradle` file in the root of your project: included in the `build.gradle` file in the root of your project:
@ -38,6 +40,8 @@ repositories {
} }
``` ```
#### 2. Add ExoPlayer module dependencies ####
Next add a dependency in the `build.gradle` file of your app module. The Next add a dependency in the `build.gradle` file of your app module. The
following will add a dependency to the full library: following will add a dependency to the full library:
@ -45,15 +49,7 @@ following will add a dependency to the full library:
implementation 'com.google.android.exoplayer:exoplayer:2.X.X' implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
``` ```
where `2.X.X` is your preferred version. If not enabled already, you also need where `2.X.X` is your preferred version.
to turn on Java 8 support in all `build.gradle` files depending on ExoPlayer, by
adding the following to the `android` section:
```gradle
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
```
As an alternative to the full library, you can depend on only the library As an alternative to the full library, you can depend on only the library
modules that you actually need. For example the following will add dependencies modules that you actually need. For example the following will add dependencies
@ -87,6 +83,32 @@ JCenter can be found on [Bintray][].
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
[Bintray]: https://bintray.com/google/exoplayer [Bintray]: https://bintray.com/google/exoplayer
#### 3. Turn on Java 8 support ####
If not enabled already, you also need to turn on Java 8 support in all
`build.gradle` files depending on ExoPlayer, by adding the following to the
`android` section:
```gradle
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
```
Note that if you want to use Java 8 features in your own code, the following
additional options need to be set:
```gradle
// For Java compilers:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
}
// For Kotlin compilers:
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
```
### Locally ### ### Locally ###
Cloning the repository and depending on the modules locally is required when Cloning the repository and depending on the modules locally is required when

View File

@ -2,6 +2,7 @@
### dev-v2 (not yet released) ### ### dev-v2 (not yet released) ###
* `ExtractorMediaSource` renamed to `ProgressiveMediaSource`.
* Support for playing spherical videos on Daydream. * Support for playing spherical videos on Daydream.
* Improve decoder re-use between playbacks. TODO: Write and link a blog post * Improve decoder re-use between playbacks. TODO: Write and link a blog post
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)). here ([#2826](https://github.com/google/ExoPlayer/issues/2826)).
@ -17,7 +18,8 @@
* Add `setStreamKeys` method to factories of DASH, SmoothStreaming and HLS * Add `setStreamKeys` method to factories of DASH, SmoothStreaming and HLS
media sources to simplify filtering by downloaded streams. media sources to simplify filtering by downloaded streams.
* Caching: * Caching:
* Improve performance of `SimpleCache`. * Improve performance of `SimpleCache`
([#4253](https://github.com/google/ExoPlayer/issues/4253)).
* Cache data with unknown length by default. The previous flag to opt in to * Cache data with unknown length by default. The previous flag to opt in to
this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
replaced with an opt out flag replaced with an opt out flag
@ -27,20 +29,70 @@
* Rename TaskState to DownloadState. * Rename TaskState to DownloadState.
* Add new states to DownloadState. * Add new states to DownloadState.
* Replace DownloadState.action with DownloadAction fields. * Replace DownloadState.action with DownloadAction fields.
* DRM: Fix black flicker when keys rotate in DRM protected content
([#3561](https://github.com/google/ExoPlayer/issues/3561)).
* Add support for SHOUTcast ICY metadata * Add support for SHOUTcast ICY metadata
([#3735](https://github.com/google/ExoPlayer/issues/3735)). ([#3735](https://github.com/google/ExoPlayer/issues/3735)).
* IMA extension: * CEA-608: Improved conformance to the specification
* Clear ads loader listeners on release ([#3860](https://github.com/google/ExoPlayer/issues/3860)).
* IMA extension: Require setting the `Player` on `AdsLoader` instances before
playback.
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.
* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to
surface YUV output as the default. Remove constructor parameters `scaleToFit`
and `useSurfaceYuvOutput`.
* Change signature of `PlayerNotificationManager.NotificationListener` to better
fit service requirements. Remove ability to set a custom stop action.
* Fix issues with flickering notifications on KitKat.
`PlayerNotificationManager` has been fixed. Apps using
`DownloadNotificationUtil` should switch to using
`DownloadNotificationHelper`.
### 2.9.5 ###
* HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag.
* ConcatenatingMediaSource:
* Add `Handler` parameter to methods that take a callback `Runnable`.
* Fix issue with dropped messages when releasing the source
([#5464](https://github.com/google/ExoPlayer/issues/5464)).
* ExtractorMediaSource: Fix issue that could cause the player to get stuck
buffering at the end of the media.
* PlayerView: Fix issue preventing `OnClickListener` from receiving events
([#5433](https://github.com/google/ExoPlayer/issues/5433)).
* IMA extension: Upgrade IMA dependency to 3.10.6.
* Cronet extension: Upgrade Cronet dependency to 71.3578.98.
* OkHttp extension: Upgrade OkHttp dependency to 3.12.1.
* MP3: Wider fix for issue where streams would play twice on some Samsung
devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)).
### 2.9.4 ###
* IMA extension: Clear ads loader listeners on release
([#4114](https://github.com/google/ExoPlayer/issues/4114)). ([#4114](https://github.com/google/ExoPlayer/issues/4114)).
* Require setting the `Player` on `AdsLoader` instances before playback. * SmoothStreaming: Fix support for subtitles in DRM protected streams
([#5378](https://github.com/google/ExoPlayer/issues/5378)).
* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior * FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior
of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)). of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)).
* GVR extension: upgrade GVR SDK dependency to 1.190.0.
* Associate fatal player errors of type SOURCE with the loading source in
`AnalyticsListener.EventTime`
([#5407](https://github.com/google/ExoPlayer/issues/5407)).
* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where
using lazy preparation in `ConcatenatingMediaSource` with an
`ExtractorMediaSource` overrides initial seek positions
([#5350](https://github.com/google/ExoPlayer/issues/5350)).
* Add subtext to the `MediaDescriptionAdapter` of the
`PlayerNotificationManager`.
* Add workaround for video quality problems with Amlogic decoders
([#5003](https://github.com/google/ExoPlayer/issues/5003)).
* Fix issue where sending callbacks for playlist changes may cause problems * Fix issue where sending callbacks for playlist changes may cause problems
because of parallel player access because of parallel player access
([#5240](https://github.com/google/ExoPlayer/issues/5240)). ([#5240](https://github.com/google/ExoPlayer/issues/5240)).
* Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a * Fix issue with reusing a `ClippingMediaSource` with an inner
callback `Runnable`. `ExtractorMediaSource` and a non-zero start position
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. ([#5351](https://github.com/google/ExoPlayer/issues/5351)).
* Fix issue where uneven track durations in MP4 streams can cause OOM problems
([#3670](https://github.com/google/ExoPlayer/issues/3670)).
### 2.9.3 ### ### 2.9.3 ###
@ -1173,7 +1225,7 @@
[here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi). [here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi).
* Robustness improvements when handling MediaSource timeline changes and * Robustness improvements when handling MediaSource timeline changes and
MediaPeriod transitions. MediaPeriod transitions.
* EIA608: Support for caption styling and positioning. * CEA-608: Support for caption styling and positioning.
* MPEG-TS: Improved support: * MPEG-TS: Improved support:
* Support injection of custom TS payload readers. * Support injection of custom TS payload readers.
* Support injection of custom section payload readers. * Support injection of custom section payload readers.
@ -1417,8 +1469,8 @@ V2 release.
(#801). (#801).
* MP3: Fix playback of some streams when stream length is unknown. * MP3: Fix playback of some streams when stream length is unknown.
* ID3: Support multiple frames of the same type in a single tag. * ID3: Support multiple frames of the same type in a single tag.
* EIA608: Correctly handle repeated control characters, fixing an issue in which * CEA-608: Correctly handle repeated control characters, fixing an issue in
captions would immediately disappear. which captions would immediately disappear.
* AVC3: Fix decoder failures on some MediaTek devices in the case where the * AVC3: Fix decoder failures on some MediaTek devices in the case where the
first buffer fed to the decoder does not start with SPS/PPS NAL units. first buffer fed to the decoder does not start with SPS/PPS NAL units.
* Misc bug fixes. * Misc bug fixes.

View File

@ -13,13 +13,9 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.9.3' releaseVersion = '2.9.5'
releaseVersionCode = 2009003 releaseVersionCode = 2009005
// Important: ExoPlayer specifies a minSdkVersion of 14 because various minSdkVersion = 16
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided
// by the library requires API level 16 or greater.
minSdkVersion = 14
targetSdkVersion = 28 targetSdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 28
buildToolsVersion = '28.0.2' buildToolsVersion = '28.0.2'

View File

@ -26,7 +26,7 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }

View File

@ -35,8 +35,8 @@ import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
@ -63,7 +63,7 @@ import java.util.ArrayList;
private final SimpleExoPlayer exoPlayer; private final SimpleExoPlayer exoPlayer;
private final CastPlayer castPlayer; private final CastPlayer castPlayer;
private final ArrayList<MediaItem> mediaQueue; private final ArrayList<MediaItem> mediaQueue;
private final QueuePositionListener queuePositionListener; private final QueueChangesListener queueChangesListener;
private final ConcatenatingMediaSource concatenatingMediaSource; private final ConcatenatingMediaSource concatenatingMediaSource;
private boolean castMediaQueueCreationPending; private boolean castMediaQueueCreationPending;
@ -71,32 +71,21 @@ import java.util.ArrayList;
private Player currentPlayer; private Player currentPlayer;
/** /**
* @param queuePositionListener A {@link QueuePositionListener} for queue position changes. * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
*
* @param queueChangesListener A {@link QueueChangesListener} for queue position changes.
* @param localPlayerView The {@link PlayerView} for local playback. * @param localPlayerView The {@link PlayerView} for local playback.
* @param castControlView The {@link PlayerControlView} to control remote playback. * @param castControlView The {@link PlayerControlView} to control remote playback.
* @param context A {@link Context}. * @param context A {@link Context}.
* @param castContext The {@link CastContext}. * @param castContext The {@link CastContext}.
*/ */
public static DefaultReceiverPlayerManager createPlayerManager( public DefaultReceiverPlayerManager(
QueuePositionListener queuePositionListener, QueueChangesListener queueChangesListener,
PlayerView localPlayerView, PlayerView localPlayerView,
PlayerControlView castControlView, PlayerControlView castControlView,
Context context, Context context,
CastContext castContext) { CastContext castContext) {
DefaultReceiverPlayerManager defaultReceiverPlayerManager = this.queueChangesListener = queueChangesListener;
new DefaultReceiverPlayerManager(
queuePositionListener, localPlayerView, castControlView, context, castContext);
defaultReceiverPlayerManager.init();
return defaultReceiverPlayerManager;
}
private DefaultReceiverPlayerManager(
QueuePositionListener queuePositionListener,
PlayerView localPlayerView,
PlayerControlView castControlView,
Context context,
CastContext castContext) {
this.queuePositionListener = queuePositionListener;
this.localPlayerView = localPlayerView; this.localPlayerView = localPlayerView;
this.castControlView = castControlView; this.castControlView = castControlView;
mediaQueue = new ArrayList<>(); mediaQueue = new ArrayList<>();
@ -113,6 +102,8 @@ import java.util.ArrayList;
castPlayer.addListener(this); castPlayer.addListener(this);
castPlayer.setSessionAvailabilityListener(this); castPlayer.setSessionAvailabilityListener(this);
castControlView.setPlayer(castPlayer); castControlView.setPlayer(castPlayer);
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
} }
// Queue manipulation methods. // Queue manipulation methods.
@ -287,10 +278,6 @@ import java.util.ArrayList;
// Internal methods. // Internal methods.
private void init() {
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
}
private void updateCurrentItemIndex() { private void updateCurrentItemIndex() {
int playbackState = currentPlayer.getPlaybackState(); int playbackState = currentPlayer.getPlaybackState();
maybeSetCurrentItemAndNotify( maybeSetCurrentItemAndNotify(
@ -372,7 +359,7 @@ import java.util.ArrayList;
if (this.currentItemIndex != currentItemIndex) { if (this.currentItemIndex != currentItemIndex) {
int oldIndex = this.currentItemIndex; int oldIndex = this.currentItemIndex;
this.currentItemIndex = currentItemIndex; this.currentItemIndex = currentItemIndex;
queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex); queueChangesListener.onQueuePositionChanged(oldIndex, currentItemIndex);
} }
} }
@ -386,7 +373,7 @@ import java.util.ArrayList;
case DemoUtil.MIME_TYPE_HLS: case DemoUtil.MIME_TYPE_HLS:
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
case DemoUtil.MIME_TYPE_VIDEO_MP4: case DemoUtil.MIME_TYPE_VIDEO_MP4:
return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
default: { default: {
throw new IllegalStateException("Unsupported type: " + item.mimeType); throw new IllegalStateException("Unsupported type: " + item.mimeType);
} }

View File

@ -15,10 +15,14 @@
*/ */
package com.google.android.exoplayer2.castdemo; package com.google.android.exoplayer2.castdemo;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID;
/** Utility methods and constants for the Cast demo application. */ /** Utility methods and constants for the Cast demo application. */
/* package */ final class DemoUtil { /* package */ final class DemoUtil {
@ -32,6 +36,16 @@ import java.util.List;
public final String name; public final String name;
/** The mime type of the sample media content. */ /** The mime type of the sample media content. */
public final String mimeType; public final String mimeType;
/**
* The {@link UUID} of the DRM scheme that protects the content, or null if the content is not
* DRM-protected.
*/
@Nullable public final UUID drmSchemeUuid;
/**
* The url from which players should obtain DRM licenses, or null if the content is not
* DRM-protected.
*/
@Nullable public final Uri licenseServerUri;
/** /**
* @param uri See {@link #uri}. * @param uri See {@link #uri}.
@ -39,9 +53,21 @@ import java.util.List;
* @param mimeType See {@link #mimeType}. * @param mimeType See {@link #mimeType}.
*/ */
public Sample(String uri, String name, String mimeType) { public Sample(String uri, String name, String mimeType) {
this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null);
}
public Sample(
String uri,
String name,
String mimeType,
@Nullable UUID drmSchemeUuid,
@Nullable String licenseServerUriString) {
this.uri = uri; this.uri = uri;
this.name = name; this.name = name;
this.mimeType = mimeType; this.mimeType = mimeType;
this.drmSchemeUuid = drmSchemeUuid;
this.licenseServerUri =
licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null;
} }
@Override @Override
@ -62,25 +88,15 @@ import java.util.List;
// App samples. // App samples.
ArrayList<Sample> samples = new ArrayList<>(); ArrayList<Sample> samples = new ArrayList<>();
// Clear content.
samples.add( samples.add(
new Sample( new Sample(
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
"DASH (clear,MP4,H264)", "Clear DASH: Tears",
MIME_TYPE_DASH)); MIME_TYPE_DASH));
samples.add( samples.add(
new Sample( new Sample(
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/" "https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4));
+ "hls/TearsOfSteel.m3u8",
"Tears of Steel (HLS)",
MIME_TYPE_HLS));
samples.add(
new Sample(
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3"
+ "/bipbop_4x3_variant.m3u8",
"HLS Basic (TS)",
MIME_TYPE_HLS));
samples.add(
new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)", MIME_TYPE_VIDEO_MP4));
SAMPLES = Collections.unmodifiableList(samples); SAMPLES = Collections.unmodifiableList(samples);
} }

View File

@ -42,13 +42,14 @@ import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.dynamite.DynamiteModule; import com.google.android.gms.dynamite.DynamiteModule;
import java.util.Collections;
/** /**
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
* Cast extension. * Cast extension.
*/ */
public class MainActivity extends AppCompatActivity public class MainActivity extends AppCompatActivity
implements OnClickListener, PlayerManager.QueuePositionListener { implements OnClickListener, PlayerManager.QueueChangesListener {
private final MediaItem.Builder mediaItemBuilder; private final MediaItem.Builder mediaItemBuilder;
@ -120,8 +121,8 @@ public class MainActivity extends AppCompatActivity
switch (applicationId) { switch (applicationId) {
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
playerManager = playerManager =
DefaultReceiverPlayerManager.createPlayerManager( new DefaultReceiverPlayerManager(
/* queuePositionListener= */ this, /* queueChangesListener= */ this,
localPlayerView, localPlayerView,
castControlView, castControlView,
/* context= */ this, /* context= */ this,
@ -161,7 +162,7 @@ public class MainActivity extends AppCompatActivity
.show(); .show();
} }
// PlayerManager.QueuePositionListener implementation. // PlayerManager.QueueChangesListener implementation.
@Override @Override
public void onQueuePositionChanged(int previousIndex, int newIndex) { public void onQueuePositionChanged(int previousIndex, int newIndex) {
@ -173,6 +174,11 @@ public class MainActivity extends AppCompatActivity
} }
} }
@Override
public void onQueueContentsExternallyChanged() {
mediaQueueListAdapter.notifyDataSetChanged();
}
// Internal methods. // Internal methods.
private View buildSampleListView() { private View buildSampleListView() {
@ -182,13 +188,18 @@ public class MainActivity extends AppCompatActivity
sampleList.setOnItemClickListener( sampleList.setOnItemClickListener(
(parent, view, position, id) -> { (parent, view, position, id) -> {
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position); DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position);
playerManager.addItem(
mediaItemBuilder mediaItemBuilder
.clear() .clear()
.setMedia(sample.uri) .setMedia(sample.uri)
.setTitle(sample.name) .setTitle(sample.name)
.setMimeType(sample.mimeType) .setMimeType(sample.mimeType);
.build()); if (sample.drmSchemeUuid != null) {
mediaItemBuilder.setDrmSchemes(
Collections.singletonList(
new MediaItem.DrmScheme(
sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri))));
}
playerManager.addItem(mediaItemBuilder.build());
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
}); });
return dialogList; return dialogList;
@ -268,6 +279,8 @@ public class MainActivity extends AppCompatActivity
int position = viewHolder.getAdapterPosition(); int position = viewHolder.getAdapterPosition();
if (playerManager.removeItem(position)) { if (playerManager.removeItem(position)) {
mediaQueueListAdapter.notifyItemRemoved(position); mediaQueueListAdapter.notifyItemRemoved(position);
// Update whichever item took its place, in case it became the new selected item.
mediaQueueListAdapter.notifyItemChanged(position);
} }
} }

View File

@ -22,14 +22,14 @@ import com.google.android.exoplayer2.ext.cast.MediaItem;
/** Manages the players in the Cast demo app. */ /** Manages the players in the Cast demo app. */
interface PlayerManager { interface PlayerManager {
/** Listener for changes in the media queue playback position. */ /** Listener for changes in the media queue. */
interface QueuePositionListener { interface QueueChangesListener {
/** /** Called when the currently played item of the media queue changes. */
* Called when the currently played item of the media queue changes.
*/
void onQueuePositionChanged(int previousIndex, int newIndex); void onQueuePositionChanged(int previousIndex, int newIndex);
/** Called when the media queue changes due to modifications not caused by this manager. */
void onQueueContentsExternallyChanged();
} }
/** Redirects the given {@code keyEvent} to the active player. */ /** Redirects the given {@code keyEvent} to the active player. */

View File

@ -26,7 +26,7 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }

View File

@ -23,16 +23,12 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
@ -56,14 +52,9 @@ import com.google.android.exoplayer2.util.Util;
} }
public void init(Context context, PlayerView playerView) { public void init(Context context, PlayerView playerView) {
// Create a default track selector.
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
// Create a player instance. // Create a player instance.
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector); player = ExoPlayerFactory.newSimpleInstance(context);
adsLoader.setPlayer(player);
// Bind the player to the view.
playerView.setPlayer(player); playerView.setPlayer(player);
// This is the MediaSource representing the content media (i.e. not the ad). // This is the MediaSource representing the content media (i.e. not the ad).
@ -89,6 +80,7 @@ import com.google.android.exoplayer2.util.Util;
contentPosition = player.getContentPosition(); contentPosition = player.getContentPosition();
player.release(); player.release();
player = null; player = null;
adsLoader.setPlayer(null);
} }
} }
@ -125,7 +117,7 @@ import com.google.android.exoplayer2.util.Util;
case C.TYPE_HLS: case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
default: default:
throw new IllegalStateException("Unsupported type: " + type); throw new IllegalStateException("Unsupported type: " + type);
} }

View File

@ -26,7 +26,7 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }

View File

@ -18,7 +18,11 @@ package com.google.android.exoplayer2.demo;
import android.app.Application; import android.app.Application;
import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
import com.google.android.exoplayer2.offline.ActionFile;
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadIndexUtil;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
@ -31,14 +35,17 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException;
/** /**
* Placeholder application to facilitate overriding Application methods for debugging and testing. * Placeholder application to facilitate overriding Application methods for debugging and testing.
*/ */
public class DemoApplication extends Application { public class DemoApplication extends Application {
private static final String TAG = "DemoApplication";
private static final String DOWNLOAD_ACTION_FILE = "actions"; private static final String DOWNLOAD_ACTION_FILE = "actions";
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
@ -97,19 +104,28 @@ public class DemoApplication extends Application {
private synchronized void initDownloadManager() { private synchronized void initDownloadManager() {
if (downloadManager == null) { if (downloadManager == null) {
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(new ExoDatabaseProvider(this));
File actionFile = new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE);
if (actionFile.exists()) {
try {
DownloadIndexUtil.upgradeActionFile(new ActionFile(actionFile), downloadIndex, null);
} catch (IOException e) {
Log.e(TAG, "Upgrading action file failed", e);
}
actionFile.delete();
}
DownloaderConstructorHelper downloaderConstructorHelper = DownloaderConstructorHelper downloaderConstructorHelper =
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
downloadManager = downloadManager =
new DownloadManager( new DownloadManager(
this,
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE), new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
new DefaultDownloaderFactory(downloaderConstructorHelper), new DefaultDownloaderFactory(downloaderConstructorHelper),
MAX_SIMULTANEOUS_DOWNLOADS, MAX_SIMULTANEOUS_DOWNLOADS,
DownloadManager.DEFAULT_MIN_RETRY_COUNT); DownloadManager.DEFAULT_MIN_RETRY_COUNT,
DownloadManager.DEFAULT_REQUIREMENTS);
downloadTracker = downloadTracker =
new DownloadTracker( new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadIndex);
/* context= */ this,
buildDataSourceFactory(),
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
downloadManager.addListener(downloadTracker); downloadManager.addListener(downloadTracker);
} }
} }

View File

@ -20,7 +20,7 @@ import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.offline.DownloadState; import com.google.android.exoplayer2.offline.DownloadState;
import com.google.android.exoplayer2.scheduler.PlatformScheduler; import com.google.android.exoplayer2.scheduler.PlatformScheduler;
import com.google.android.exoplayer2.ui.DownloadNotificationUtil; import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.NotificationUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
@ -33,6 +33,8 @@ public class DemoDownloadService extends DownloadService {
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
private DownloadNotificationHelper notificationHelper;
public DemoDownloadService() { public DemoDownloadService() {
super( super(
FOREGROUND_NOTIFICATION_ID, FOREGROUND_NOTIFICATION_ID,
@ -42,6 +44,12 @@ public class DemoDownloadService extends DownloadService {
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
} }
@Override
public void onCreate() {
super.onCreate();
notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);
}
@Override @Override
protected DownloadManager getDownloadManager() { protected DownloadManager getDownloadManager() {
return ((DemoApplication) getApplication()).getDownloadManager(); return ((DemoApplication) getApplication()).getDownloadManager();
@ -54,32 +62,23 @@ public class DemoDownloadService extends DownloadService {
@Override @Override
protected Notification getForegroundNotification(DownloadState[] downloadStates) { protected Notification getForegroundNotification(DownloadState[] downloadStates) {
return DownloadNotificationUtil.buildProgressNotification( return notificationHelper.buildProgressNotification(
/* context= */ this, R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloadStates);
R.drawable.ic_download,
CHANNEL_ID,
/* contentIntent= */ null,
/* message= */ null,
downloadStates);
} }
@Override @Override
protected void onDownloadStateChanged(DownloadState downloadState) { protected void onDownloadStateChanged(DownloadState downloadState) {
Notification notification = null; Notification notification;
if (downloadState.state == DownloadState.STATE_COMPLETED) { if (downloadState.state == DownloadState.STATE_COMPLETED) {
notification = notification =
DownloadNotificationUtil.buildDownloadCompletedNotification( notificationHelper.buildDownloadCompletedNotification(
/* context= */ this,
R.drawable.ic_download_done, R.drawable.ic_download_done,
CHANNEL_ID,
/* contentIntent= */ null, /* contentIntent= */ null,
Util.fromUtf8Bytes(downloadState.customMetadata)); Util.fromUtf8Bytes(downloadState.customMetadata));
} else if (downloadState.state == DownloadState.STATE_FAILED) { } else if (downloadState.state == DownloadState.STATE_FAILED) {
notification = notification =
DownloadNotificationUtil.buildDownloadFailedNotification( notificationHelper.buildDownloadFailedNotification(
/* context= */ this,
R.drawable.ic_download_done, R.drawable.ic_download_done,
CHANNEL_ID,
/* contentIntent= */ null, /* contentIntent= */ null,
Util.fromUtf8Bytes(downloadState.customMetadata)); Util.fromUtf8Bytes(downloadState.customMetadata));
} else { } else {

View File

@ -34,17 +34,16 @@ import android.widget.Toast;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.ActionFile; import com.google.android.exoplayer2.offline.ActionFile;
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.offline.DownloadState; import com.google.android.exoplayer2.offline.DownloadState;
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; import com.google.android.exoplayer2.offline.DownloadStateCursor;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper;
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper;
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
@ -54,8 +53,8 @@ import com.google.android.exoplayer2.ui.TrackSelectionView;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -83,20 +82,21 @@ public class DownloadTracker implements DownloadManager.Listener {
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
private final TrackNameProvider trackNameProvider; private final TrackNameProvider trackNameProvider;
private final CopyOnWriteArraySet<Listener> listeners; private final CopyOnWriteArraySet<Listener> listeners;
private final HashMap<Uri, DownloadAction> trackedDownloadStates; private final HashMap<Uri, DownloadState> trackedDownloadStates;
private final ActionFile actionFile; private final DefaultDownloadIndex downloadIndex;
private final Handler actionFileWriteHandler; private final Handler actionFileIOHandler;
public DownloadTracker(Context context, DataSource.Factory dataSourceFactory, File actionFile) { public DownloadTracker(
Context context, DataSource.Factory dataSourceFactory, DefaultDownloadIndex downloadIndex) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.actionFile = new ActionFile(actionFile); this.downloadIndex = downloadIndex;
trackNameProvider = new DefaultTrackNameProvider(context.getResources()); trackNameProvider = new DefaultTrackNameProvider(context.getResources());
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
trackedDownloadStates = new HashMap<>(); trackedDownloadStates = new HashMap<>();
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker"); HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
actionFileWriteThread.start(); actionFileWriteThread.start();
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper()); actionFileIOHandler = new Handler(actionFileWriteThread.getLooper());
loadTrackedActions(); loadTrackedActions();
} }
@ -117,7 +117,7 @@ public class DownloadTracker implements DownloadManager.Listener {
if (!trackedDownloadStates.containsKey(uri)) { if (!trackedDownloadStates.containsKey(uri)) {
return Collections.emptyList(); return Collections.emptyList();
} }
return trackedDownloadStates.get(uri).getKeys(); return Arrays.asList(trackedDownloadStates.get(uri).streamKeys);
} }
public void toggleDownload( public void toggleDownload(
@ -149,7 +149,7 @@ public class DownloadTracker implements DownloadManager.Listener {
|| downloadState.state == DownloadState.STATE_FAILED) { || downloadState.state == DownloadState.STATE_FAILED) {
// A download has been removed, or has failed. Stop tracking it. // A download has been removed, or has failed. Stop tracking it.
if (trackedDownloadStates.remove(downloadState.uri) != null) { if (trackedDownloadStates.remove(downloadState.uri) != null) {
handleTrackedDownloadStatesChanged(); handleTrackedDownloadStateChanged(downloadState);
} }
} }
} }
@ -159,30 +159,35 @@ public class DownloadTracker implements DownloadManager.Listener {
// Do nothing. // Do nothing.
} }
@Override
public void onRequirementsStateChanged(
DownloadManager downloadManager,
Requirements requirements,
@Requirements.RequirementFlags int notMetRequirements) {
// Do nothing.
}
// Internal methods // Internal methods
private void loadTrackedActions() { private void loadTrackedActions() {
try { DownloadStateCursor downloadStates = downloadIndex.getDownloadStates();
DownloadAction[] allActions = actionFile.load(); while (downloadStates.moveToNext()) {
for (DownloadAction action : allActions) { DownloadState downloadState = downloadStates.getDownloadState();
trackedDownloadStates.put(action.uri, action); trackedDownloadStates.put(downloadState.uri, downloadState);
}
} catch (IOException e) {
Log.e(TAG, "Failed to load tracked actions", e);
} }
downloadStates.close();
} }
private void handleTrackedDownloadStatesChanged() { private void handleTrackedDownloadStateChanged(DownloadState downloadState) {
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onDownloadsChanged(); listener.onDownloadsChanged();
} }
final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]); actionFileIOHandler.post(
actionFileWriteHandler.post(
() -> { () -> {
try { if (downloadState.state == DownloadState.STATE_REMOVED) {
actionFile.store(actions); downloadIndex.removeDownloadState(downloadState.id);
} catch (IOException e) { } else {
Log.e(TAG, "Failed to store tracked actions", e); downloadIndex.putDownloadState(downloadState);
} }
}); });
} }
@ -192,8 +197,9 @@ public class DownloadTracker implements DownloadManager.Listener {
// This content is already being downloaded. Do nothing. // This content is already being downloaded. Do nothing.
return; return;
} }
trackedDownloadStates.put(action.uri, action); DownloadState downloadState = new DownloadState(action);
handleTrackedDownloadStatesChanged(); trackedDownloadStates.put(downloadState.uri, downloadState);
handleTrackedDownloadStateChanged(downloadState);
startServiceWithAction(action); startServiceWithAction(action);
} }
@ -201,18 +207,18 @@ public class DownloadTracker implements DownloadManager.Listener {
DownloadService.startWithAction(context, DemoDownloadService.class, action, false); DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
} }
private DownloadHelper<?> getDownloadHelper( private DownloadHelper getDownloadHelper(
Uri uri, String extension, RenderersFactory renderersFactory) { Uri uri, String extension, RenderersFactory renderersFactory) {
int type = Util.inferContentType(uri, extension); int type = Util.inferContentType(uri, extension);
switch (type) { switch (type) {
case C.TYPE_DASH: case C.TYPE_DASH:
return new DashDownloadHelper(uri, dataSourceFactory, renderersFactory); return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory);
case C.TYPE_SS: case C.TYPE_SS:
return new SsDownloadHelper(uri, dataSourceFactory, renderersFactory); return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory);
case C.TYPE_HLS: case C.TYPE_HLS:
return new HlsDownloadHelper(uri, dataSourceFactory, renderersFactory); return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return new ProgressiveDownloadHelper(uri); return DownloadHelper.forProgressive(uri);
default: default:
throw new IllegalStateException("Unsupported type: " + type); throw new IllegalStateException("Unsupported type: " + type);
} }
@ -222,10 +228,11 @@ public class DownloadTracker implements DownloadManager.Listener {
private final class StartDownloadDialogHelper private final class StartDownloadDialogHelper
implements DownloadHelper.Callback, implements DownloadHelper.Callback,
DialogInterface.OnClickListener, DialogInterface.OnClickListener,
DialogInterface.OnDismissListener,
View.OnClickListener, View.OnClickListener,
TrackSelectionView.DialogCallback { TrackSelectionView.DialogCallback {
private final DownloadHelper<?> downloadHelper; private final DownloadHelper downloadHelper;
private final String name; private final String name;
private final LayoutInflater dialogInflater; private final LayoutInflater dialogInflater;
private final AlertDialog dialog; private final AlertDialog dialog;
@ -235,20 +242,21 @@ public class DownloadTracker implements DownloadManager.Listener {
private DefaultTrackSelector.Parameters parameters; private DefaultTrackSelector.Parameters parameters;
private StartDownloadDialogHelper( private StartDownloadDialogHelper(
Activity activity, DownloadHelper<?> downloadHelper, String name) { Activity activity, DownloadHelper downloadHelper, String name) {
this.downloadHelper = downloadHelper; this.downloadHelper = downloadHelper;
this.name = name; this.name = name;
AlertDialog.Builder builder = AlertDialog.Builder builder =
new AlertDialog.Builder(activity) new AlertDialog.Builder(activity)
.setTitle(R.string.download_preparing) .setTitle(R.string.download_preparing)
.setPositiveButton(android.R.string.ok, this) .setPositiveButton(android.R.string.ok, /* listener= */ this)
.setNegativeButton(android.R.string.cancel, null); .setNegativeButton(android.R.string.cancel, /* listener= */ null);
// Inflate with the builder's context to ensure the correct style is used. // Inflate with the builder's context to ensure the correct style is used.
dialogInflater = LayoutInflater.from(builder.getContext()); dialogInflater = LayoutInflater.from(builder.getContext());
selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null); selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null);
builder.setView(selectionList); builder.setView(selectionList);
dialog = builder.create(); dialog = builder.create();
dialog.setOnDismissListener(/* listener= */ this);
dialog.show(); dialog.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
@ -259,19 +267,17 @@ public class DownloadTracker implements DownloadManager.Listener {
// DownloadHelper.Callback implementation. // DownloadHelper.Callback implementation.
@Override @Override
public void onPrepared(DownloadHelper<?> helper) { public void onPrepared(DownloadHelper helper) {
if (helper.getPeriodCount() < 1) { if (helper.getPeriodCount() > 0) {
onPrepareError(downloadHelper, new IOException("Content is empty."));
return;
}
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
updateSelectionList(); updateSelectionList();
}
dialog.setTitle(R.string.exo_download_description); dialog.setTitle(R.string.exo_download_description);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
} }
@Override @Override
public void onPrepareError(DownloadHelper<?> helper, IOException e) { public void onPrepareError(DownloadHelper helper, IOException e) {
Toast.makeText( Toast.makeText(
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
.show(); .show();
@ -317,6 +323,13 @@ public class DownloadTracker implements DownloadManager.Listener {
startDownload(downloadAction); startDownload(downloadAction);
} }
// DialogInterface.OnDismissListener implementation.
@Override
public void onDismiss(DialogInterface dialog) {
downloadHelper.release();
}
// Internal methods. // Internal methods.
private void updateSelectionList() { private void updateSelectionList() {

View File

@ -51,8 +51,8 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
@ -483,7 +483,7 @@ public class PlayerActivity extends Activity
.setStreamKeys(offlineStreamKeys) .setStreamKeys(offlineStreamKeys)
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
default: { default: {
throw new IllegalStateException("Unsupported type: " + type); throw new IllegalStateException("Unsupported type: " + type);
} }

View File

@ -24,7 +24,7 @@ android {
} }
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
} }

View File

@ -19,7 +19,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
@ -30,7 +30,7 @@ android {
} }
dependencies { dependencies {
api 'org.chromium.net:cronet-embedded:66.3359.158' api 'org.chromium.net:cronet-embedded:71.3578.98'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'library')

View File

@ -28,8 +28,8 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before; import org.junit.Before;
@ -86,7 +86,7 @@ public class FlacPlaybackTest {
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector); player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
MediaSource mediaSource = MediaSource mediaSource =
new ExtractorMediaSource.Factory( new ProgressiveMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
.setExtractorsFactory(MatroskaExtractor.FACTORY) .setExtractorsFactory(MatroskaExtractor.FACTORY)
.createMediaSource(uri); .createMediaSource(uri);

View File

@ -33,9 +33,7 @@ dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation 'com.google.vr:sdk-audio:1.80.0' api 'com.google.vr:sdk-base:1.190.0'
implementation 'com.google.vr:sdk-controller:1.80.0'
api 'com.google.vr:sdk-base:1.80.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
} }

View File

@ -31,13 +31,13 @@ android {
} }
dependencies { dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.2' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.6'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.google.android.gms:play-services-ads:17.1.1' implementation 'com.google.android.gms:play-services-ads:17.1.2'
// These dependencies are necessary to force the supportLibraryVersion of // These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4 and com.android.support:customtabs to be // com.android.support:support-v4 and com.android.support:customtabs to be
// used. Else older versions are used, for example via: // used. Else older versions are used, for example via:
// com.google.android.gms:play-services-ads:17.1.1 // com.google.android.gms:play-services-ads:17.1.2
// |-- com.android.support:customtabs:26.1.0 // |-- com.android.support:customtabs:26.1.0
implementation 'com.android.support:support-v4:' + supportLibraryVersion implementation 'com.android.support:support-v4:' + supportLibraryVersion
implementation 'com.android.support:customtabs:' + supportLibraryVersion implementation 'com.android.support:customtabs:' + supportLibraryVersion

View File

@ -466,11 +466,11 @@ public final class ImaAdsLoader
} }
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings);
period = new Timeline.Period(); period = new Timeline.Period();
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
adDisplayContainer = imaFactory.createAdDisplayContainer(); adDisplayContainer = imaFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(/* adErrorListener= */ this); adsLoader.addAdErrorListener(/* adErrorListener= */ this);
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
@ -524,7 +524,6 @@ public final class ImaAdsLoader
if (vastLoadTimeoutMs != TIMEOUT_UNSET) { if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
request.setVastLoadTimeout(vastLoadTimeoutMs); request.setVastLoadTimeout(vastLoadTimeoutMs);
} }
request.setAdDisplayContainer(adDisplayContainer);
request.setContentProgressProvider(this); request.setContentProgressProvider(this);
request.setUserRequestContext(pendingAdRequestContext); request.setUserRequestContext(pendingAdRequestContext);
adsLoader.requestAds(request); adsLoader.requestAds(request);
@ -1374,9 +1373,9 @@ public final class ImaAdsLoader
AdDisplayContainer createAdDisplayContainer(); AdDisplayContainer createAdDisplayContainer();
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */ /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */
AdsRequest createAdsRequest(); AdsRequest createAdsRequest();
/** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings) */ /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */
com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
Context context, ImaSdkSettings imaSdkSettings); Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
} }
/** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */ /** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */
@ -1403,8 +1402,9 @@ public final class ImaAdsLoader
@Override @Override
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
Context context, ImaSdkSettings imaSdkSettings) { Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
return ImaSdkFactory.getInstance().createAdsLoader(context, imaSdkSettings); return ImaSdkFactory.getInstance()
.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
} }
} }
} }

View File

@ -93,8 +93,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
return adsMediaSource.createPeriod(id, allocator); return adsMediaSource.createPeriod(id, allocator, startPositionUs);
} }
@Override @Override

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.ima;
import android.content.Context; import android.content.Context;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdsLoader;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest; import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
@ -64,8 +65,8 @@ final class SingletonImaFactory implements ImaAdsLoader.ImaFactory {
} }
@Override @Override
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( public AdsLoader createAdsLoader(
Context context, ImaSdkSettings imaSdkSettings) { Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
return adsLoader; return adsLoader;
} }
} }

View File

@ -34,7 +34,7 @@ dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
api 'com.squareup.okhttp3:okhttp:3.11.0' api 'com.squareup.okhttp3:okhttp:3.12.1'
} }
ext { ext {

View File

@ -28,8 +28,8 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before; import org.junit.Before;
@ -86,7 +86,7 @@ public class OpusPlaybackTest {
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector); player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
MediaSource mediaSource = MediaSource mediaSource =
new ExtractorMediaSource.Factory( new ProgressiveMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"))
.setExtractorsFactory(MatroskaExtractor.FACTORY) .setExtractorsFactory(MatroskaExtractor.FACTORY)
.createMediaSource(uri); .createMediaSource(uri);

View File

@ -24,7 +24,7 @@ android {
} }
defaultConfig { defaultConfig {
minSdkVersion 15 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
} }

View File

@ -34,26 +34,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
NDK_PATH="<path to Android NDK>" NDK_PATH="<path to Android NDK>"
``` ```
* Fetch libvpx and libyuv:
```
cd "${VP9_EXT_PATH}/jni" && \
git clone https://chromium.googlesource.com/webm/libvpx libvpx && \
git clone https://chromium.googlesource.com/libyuv/libyuv libyuv
```
* Checkout the appropriate branches of libvpx and libyuv (the scripts and
makefiles bundled in this repo are known to work only at these versions of the
libraries - we will update this periodically as newer versions of
libvpx/libyuv are released):
```
cd "${VP9_EXT_PATH}/jni/libvpx" && \
git checkout tags/v1.7.0 -b v1.7.0 && \
cd "${VP9_EXT_PATH}/jni/libyuv" && \
git checkout 996a2bbd
```
* Run a script that generates necessary configuration files for libvpx: * Run a script that generates necessary configuration files for libvpx:
``` ```
@ -78,10 +58,6 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
* Android config scripts should be re-generated by running * Android config scripts should be re-generated by running
`generate_libvpx_android_configs.sh` `generate_libvpx_android_configs.sh`
* Clean and re-build the project. * Clean and re-build the project.
* If you want to use your own version of libvpx or libyuv, place it in
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
please note that `generate_libvpx_android_configs.sh` and the makefiles need
to be modified to work with arbitrary versions of libvpx and libyuv.
## Using the extension ## ## Using the extension ##

View File

@ -29,8 +29,8 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
@ -114,12 +114,12 @@ public class VpxPlaybackTest {
@Override @Override
public void run() { public void run() {
Looper.prepare(); Looper.prepare();
LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0); LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0);
DefaultTrackSelector trackSelector = new DefaultTrackSelector(); DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector); player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
MediaSource mediaSource = MediaSource mediaSource =
new ExtractorMediaSource.Factory( new ProgressiveMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"))
.setExtractorsFactory(MatroskaExtractor.FACTORY) .setExtractorsFactory(MatroskaExtractor.FACTORY)
.createMediaSource(uri); .createMediaSource(uri);

View File

@ -15,8 +15,6 @@
*/ */
package com.google.android.exoplayer2.ext.vp9; package com.google.android.exoplayer2.ext.vp9;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock; import android.os.SystemClock;
@ -109,7 +107,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
/** The default input buffer size. */ /** The default input buffer size. */
private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
private final boolean scaleToFit;
private final boolean disableLoopFilter; private final boolean disableLoopFilter;
private final long allowedJoiningTimeMs; private final long allowedJoiningTimeMs;
private final int maxDroppedFramesToNotify; private final int maxDroppedFramesToNotify;
@ -119,7 +116,6 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private final TimedValueQueue<Format> formatQueue; private final TimedValueQueue<Format> formatQueue;
private final DecoderInputBuffer flagsOnlyBuffer; private final DecoderInputBuffer flagsOnlyBuffer;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager; private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private final boolean useSurfaceYuvOutput;
private Format format; private Format format;
private Format pendingFormat; private Format pendingFormat;
@ -127,13 +123,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private VpxDecoder decoder; private VpxDecoder decoder;
private VpxInputBuffer inputBuffer; private VpxInputBuffer inputBuffer;
private VpxOutputBuffer outputBuffer; private VpxOutputBuffer outputBuffer;
private DrmSession<ExoMediaCrypto> drmSession; @Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
private DrmSession<ExoMediaCrypto> pendingDrmSession; @Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;
private @ReinitializationState int decoderReinitializationState; private @ReinitializationState int decoderReinitializationState;
private boolean decoderReceivedBuffers; private boolean decoderReceivedBuffers;
private Bitmap bitmap;
private boolean renderedFirstFrame; private boolean renderedFirstFrame;
private long initialPositionUs; private long initialPositionUs;
private long joiningDeadlineMs; private long joiningDeadlineMs;
@ -158,16 +153,14 @@ public class LibvpxVideoRenderer extends BaseRenderer {
protected DecoderCounters decoderCounters; protected DecoderCounters decoderCounters;
/** /**
* @param scaleToFit Whether video frames should be scaled to fit when rendering.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback. * can attempt to seamlessly join an ongoing playback.
*/ */
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs) { public LibvpxVideoRenderer(long allowedJoiningTimeMs) {
this(scaleToFit, allowedJoiningTimeMs, null, null, 0); this(allowedJoiningTimeMs, null, null, 0);
} }
/** /**
* @param scaleToFit Whether video frames should be scaled to fit when rendering.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback. * can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
@ -176,23 +169,22 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
*/ */
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs, public LibvpxVideoRenderer(
Handler eventHandler, VideoRendererEventListener eventListener, long allowedJoiningTimeMs,
Handler eventHandler,
VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) { int maxDroppedFramesToNotify) {
this( this(
scaleToFit,
allowedJoiningTimeMs, allowedJoiningTimeMs,
eventHandler, eventHandler,
eventListener, eventListener,
maxDroppedFramesToNotify, maxDroppedFramesToNotify,
/* drmSessionManager= */ null, /* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false, /* playClearSamplesWithoutKeys= */ false,
/* disableLoopFilter= */ false, /* disableLoopFilter= */ false);
/* useSurfaceYuvOutput= */ false);
} }
/** /**
* @param scaleToFit Whether video frames should be scaled to fit when rendering.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback. * can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
@ -208,26 +200,21 @@ public class LibvpxVideoRenderer extends BaseRenderer {
* permitted to play clear regions of encrypted media files before {@code drmSessionManager} * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media. * has obtained the keys necessary to decrypt encrypted regions of the media.
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter. * @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
* @param useSurfaceYuvOutput Directly output YUV to the Surface via ANativeWindow.
*/ */
public LibvpxVideoRenderer( public LibvpxVideoRenderer(
boolean scaleToFit,
long allowedJoiningTimeMs, long allowedJoiningTimeMs,
Handler eventHandler, Handler eventHandler,
VideoRendererEventListener eventListener, VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify, int maxDroppedFramesToNotify,
DrmSessionManager<ExoMediaCrypto> drmSessionManager, DrmSessionManager<ExoMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys, boolean playClearSamplesWithoutKeys,
boolean disableLoopFilter, boolean disableLoopFilter) {
boolean useSurfaceYuvOutput) {
super(C.TRACK_TYPE_VIDEO); super(C.TRACK_TYPE_VIDEO);
this.scaleToFit = scaleToFit;
this.disableLoopFilter = disableLoopFilter; this.disableLoopFilter = disableLoopFilter;
this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.allowedJoiningTimeMs = allowedJoiningTimeMs;
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
this.useSurfaceYuvOutput = useSurfaceYuvOutput;
joiningDeadlineMs = C.TIME_UNSET; joiningDeadlineMs = C.TIME_UNSET;
clearReportedVideoSize(); clearReportedVideoSize();
formatHolder = new FormatHolder(); formatHolder = new FormatHolder();
@ -364,26 +351,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
clearReportedVideoSize(); clearReportedVideoSize();
clearRenderedFirstFrame(); clearRenderedFirstFrame();
try { try {
setSourceDrmSession(null);
releaseDecoder(); releaseDecoder();
} finally { } finally {
try {
if (drmSession != null) {
drmSessionManager.releaseSession(drmSession);
}
} finally {
try {
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} finally {
drmSession = null;
pendingDrmSession = null;
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters); eventDispatcher.disabled(decoderCounters);
} }
} }
}
}
@Override @Override
protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException {
@ -433,18 +406,35 @@ public class LibvpxVideoRenderer extends BaseRenderer {
/** Releases the decoder. */ /** Releases the decoder. */
@CallSuper @CallSuper
protected void releaseDecoder() { protected void releaseDecoder() {
if (decoder == null) {
return;
}
inputBuffer = null; inputBuffer = null;
outputBuffer = null; outputBuffer = null;
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false; decoderReceivedBuffers = false;
buffersInCodecCount = 0; buffersInCodecCount = 0;
if (decoder != null) {
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
}
setDecoderDrmSession(null);
}
private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
DrmSession<ExoMediaCrypto> previous = sourceDrmSession;
sourceDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
DrmSession<ExoMediaCrypto> previous = decoderDrmSession;
decoderDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void releaseDrmSessionIfUnused(@Nullable DrmSession<ExoMediaCrypto> session) {
if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
drmSessionManager.releaseSession(session);
}
} }
/** /**
@ -467,16 +457,20 @@ public class LibvpxVideoRenderer extends BaseRenderer {
throw ExoPlaybackException.createForRenderer( throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
} }
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData); DrmSession<ExoMediaCrypto> session =
if (pendingDrmSession == drmSession) { drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
drmSessionManager.releaseSession(pendingDrmSession); if (session == decoderDrmSession || session == sourceDrmSession) {
// We already had this session. The manager must be reference counting, so release it once
// to get the count attributed to this renderer back down to 1.
drmSessionManager.releaseSession(session);
} }
setSourceDrmSession(session);
} else { } else {
pendingDrmSession = null; setSourceDrmSession(null);
} }
} }
if (pendingDrmSession != drmSession) { if (sourceDrmSession != decoderDrmSession) {
if (decoderReceivedBuffers) { if (decoderReceivedBuffers) {
// Signal end of stream and wait for any final output buffers before re-initialization. // Signal end of stream and wait for any final output buffers before re-initialization.
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
@ -579,18 +573,14 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*/ */
protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException { protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException {
int bufferMode = outputBuffer.mode; int bufferMode = outputBuffer.mode;
boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null; boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null;
boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null; boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
if (!renderRgb && !renderYuv && !renderSurface) { if (!renderYuv && !renderSurface) {
dropOutputBuffer(outputBuffer); dropOutputBuffer(outputBuffer);
} else { } else {
maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height); maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
if (renderRgb) { if (renderYuv) {
renderRgbFrame(outputBuffer, scaleToFit);
outputBuffer.release();
} else if (renderYuv) {
outputBufferRenderer.setOutputBuffer(outputBuffer); outputBufferRenderer.setOutputBuffer(outputBuffer);
// The renderer will release the buffer. // The renderer will release the buffer.
} else { // renderSurface } else { // renderSurface
@ -668,8 +658,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
this.surface = surface; this.surface = surface;
this.outputBufferRenderer = outputBufferRenderer; this.outputBufferRenderer = outputBufferRenderer;
if (surface != null) { if (surface != null) {
outputMode = outputMode = VpxDecoder.OUTPUT_MODE_SURFACE_YUV;
useSurfaceYuvOutput ? VpxDecoder.OUTPUT_MODE_SURFACE_YUV : VpxDecoder.OUTPUT_MODE_RGB;
} else { } else {
outputMode = outputMode =
outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE; outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE;
@ -704,12 +693,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
return; return;
} }
drmSession = pendingDrmSession; setDecoderDrmSession(sourceDrmSession);
ExoMediaCrypto mediaCrypto = null; ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) { if (decoderDrmSession != null) {
mediaCrypto = drmSession.getMediaCrypto(); mediaCrypto = decoderDrmSession.getMediaCrypto();
if (mediaCrypto == null) { if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError(); DrmSessionException drmError = decoderDrmSession.getError();
if (drmError != null) { if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a new // Continue for now. We may be able to avoid failure if the session recovers, or if a new
// input format causes the session to be replaced before it's used. // input format causes the session to be replaced before it's used.
@ -731,8 +721,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
NUM_OUTPUT_BUFFERS, NUM_OUTPUT_BUFFERS,
initialInputBufferSize, initialInputBufferSize,
mediaCrypto, mediaCrypto,
disableLoopFilter, disableLoopFilter);
useSurfaceYuvOutput);
decoder.setOutputMode(outputMode); decoder.setOutputMode(outputMode);
TraceUtil.endSection(); TraceUtil.endSection();
long decoderInitializedTimestamp = SystemClock.elapsedRealtime(); long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
@ -922,33 +911,16 @@ public class LibvpxVideoRenderer extends BaseRenderer {
} }
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false; return false;
} }
@DrmSession.State int drmSessionState = drmSession.getState(); @DrmSession.State int drmSessionState = decoderDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) { if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
} }
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
} }
private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) {
if (bitmap == null
|| bitmap.getWidth() != outputBuffer.width
|| bitmap.getHeight() != outputBuffer.height) {
bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565);
}
bitmap.copyPixelsFromBuffer(outputBuffer.data);
Canvas canvas = surface.lockCanvas(null);
if (scale) {
canvas.scale(
((float) canvas.getWidth()) / outputBuffer.width,
((float) canvas.getHeight()) / outputBuffer.height);
}
canvas.drawBitmap(bitmap, 0, 0, null);
surface.unlockCanvasAndPost(canvas);
}
private void setJoiningDeadlineMs() { private void setJoiningDeadlineMs() {
joiningDeadlineMs = allowedJoiningTimeMs > 0 joiningDeadlineMs = allowedJoiningTimeMs > 0
? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;

View File

@ -31,8 +31,7 @@ import java.nio.ByteBuffer;
public static final int OUTPUT_MODE_NONE = -1; public static final int OUTPUT_MODE_NONE = -1;
public static final int OUTPUT_MODE_YUV = 0; public static final int OUTPUT_MODE_YUV = 0;
public static final int OUTPUT_MODE_RGB = 1; public static final int OUTPUT_MODE_SURFACE_YUV = 1;
public static final int OUTPUT_MODE_SURFACE_YUV = 2;
private static final int NO_ERROR = 0; private static final int NO_ERROR = 0;
private static final int DECODE_ERROR = 1; private static final int DECODE_ERROR = 1;
@ -52,7 +51,6 @@ import java.nio.ByteBuffer;
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
* content. Maybe null and can be ignored if decoder does not handle encrypted content. * content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter. * @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
* @param enableSurfaceYuvOutputMode Whether OUTPUT_MODE_SURFACE_YUV is allowed.
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder. * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
*/ */
public VpxDecoder( public VpxDecoder(
@ -60,8 +58,7 @@ import java.nio.ByteBuffer;
int numOutputBuffers, int numOutputBuffers,
int initialInputBufferSize, int initialInputBufferSize,
ExoMediaCrypto exoMediaCrypto, ExoMediaCrypto exoMediaCrypto,
boolean disableLoopFilter, boolean disableLoopFilter)
boolean enableSurfaceYuvOutputMode)
throws VpxDecoderException { throws VpxDecoderException {
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) { if (!VpxLibrary.isAvailable()) {
@ -71,7 +68,7 @@ import java.nio.ByteBuffer;
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) { if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
throw new VpxDecoderException("Vpx decoder does not support secure decode."); throw new VpxDecoderException("Vpx decoder does not support secure decode.");
} }
vpxDecContext = vpxInit(disableLoopFilter, enableSurfaceYuvOutputMode); vpxDecContext = vpxInit(disableLoopFilter);
if (vpxDecContext == 0) { if (vpxDecContext == 0) {
throw new VpxDecoderException("Failed to initialize decoder"); throw new VpxDecoderException("Failed to initialize decoder");
} }
@ -86,8 +83,8 @@ import java.nio.ByteBuffer;
/** /**
* Sets the output mode for frames rendered by the decoder. * Sets the output mode for frames rendered by the decoder.
* *
* @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE}, {@link #OUTPUT_MODE_RGB} * @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE} and {@link
* and {@link #OUTPUT_MODE_YUV}. * #OUTPUT_MODE_YUV}.
*/ */
public void setOutputMode(int outputMode) { public void setOutputMode(int outputMode) {
this.outputMode = outputMode; this.outputMode = outputMode;
@ -168,7 +165,7 @@ import java.nio.ByteBuffer;
} }
} }
private native long vpxInit(boolean disableLoopFilter, boolean enableSurfaceYuvOutputMode); private native long vpxInit(boolean disableLoopFilter);
private native long vpxClose(long context); private native long vpxClose(long context);
private native long vpxDecode(long context, ByteBuffer encoded, int length); private native long vpxDecode(long context, ByteBuffer encoded, int length);

View File

@ -60,36 +60,19 @@ public final class VpxOutputBuffer extends OutputBuffer {
* Initializes the buffer. * Initializes the buffer.
* *
* @param timeUs The presentation timestamp for the buffer, in microseconds. * @param timeUs The presentation timestamp for the buffer, in microseconds.
* @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE}, * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE} and {@link
* {@link VpxDecoder#OUTPUT_MODE_RGB} and {@link VpxDecoder#OUTPUT_MODE_YUV}. * VpxDecoder#OUTPUT_MODE_YUV}.
*/ */
public void init(long timeUs, int mode) { public void init(long timeUs, int mode) {
this.timeUs = timeUs; this.timeUs = timeUs;
this.mode = mode; this.mode = mode;
} }
/**
* Resizes the buffer based on the given dimensions. Called via JNI after decoding completes.
* @return Whether the buffer was resized successfully.
*/
public boolean initForRgbFrame(int width, int height) {
this.width = width;
this.height = height;
this.yuvPlanes = null;
if (!isSafeToMultiply(width, height) || !isSafeToMultiply(width * height, 2)) {
return false;
}
int minimumRgbSize = width * height * 2;
initData(minimumRgbSize);
return true;
}
/** /**
* Resizes the buffer based on the given stride. Called via JNI after decoding completes. * Resizes the buffer based on the given stride. Called via JNI after decoding completes.
*
* @return Whether the buffer was resized successfully. * @return Whether the buffer was resized successfully.
*/ */
public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) {
int colorspace) {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.colorspace = colorspace; this.colorspace = colorspace;

View File

@ -17,12 +17,6 @@
WORKING_DIR := $(call my-dir) WORKING_DIR := $(call my-dir)
include $(CLEAR_VARS) include $(CLEAR_VARS)
LIBVPX_ROOT := $(WORKING_DIR)/libvpx LIBVPX_ROOT := $(WORKING_DIR)/libvpx
LIBYUV_ROOT := $(WORKING_DIR)/libyuv
# build libyuv_static.a
LOCAL_PATH := $(WORKING_DIR)
LIBYUV_DISABLE_JPEG := "yes"
include $(LIBYUV_ROOT)/Android.mk
# build libvpx.so # build libvpx.so
LOCAL_PATH := $(WORKING_DIR) LOCAL_PATH := $(WORKING_DIR)
@ -37,7 +31,7 @@ LOCAL_CPP_EXTENSION := .cc
LOCAL_SRC_FILES := vpx_jni.cc LOCAL_SRC_FILES := vpx_jni.cc
LOCAL_LDLIBS := -llog -lz -lm -landroid LOCAL_LDLIBS := -llog -lz -lm -landroid
LOCAL_SHARED_LIBRARIES := libvpx LOCAL_SHARED_LIBRARIES := libvpx
LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures LOCAL_STATIC_LIBRARIES := cpufeatures
include $(BUILD_SHARED_LIBRARY) include $(BUILD_SHARED_LIBRARY)
$(call import-module,android/cpufeatures) $(call import-module,android/cpufeatures)

View File

@ -30,8 +30,6 @@
#include <cstring> #include <cstring>
#include <new> #include <new>
#include "libyuv.h" // NOLINT
#define VPX_CODEC_DISABLE_COMPAT 1 #define VPX_CODEC_DISABLE_COMPAT 1
#include "vpx/vpx_decoder.h" #include "vpx/vpx_decoder.h"
#include "vpx/vp8dx.h" #include "vpx/vp8dx.h"
@ -61,7 +59,6 @@
(JNIEnv* env, jobject thiz, ##__VA_ARGS__)\ (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\
// JNI references for VpxOutputBuffer class. // JNI references for VpxOutputBuffer class.
static jmethodID initForRgbFrame;
static jmethodID initForYuvFrame; static jmethodID initForYuvFrame;
static jfieldID dataField; static jfieldID dataField;
static jfieldID outputModeField; static jfieldID outputModeField;
@ -393,11 +390,7 @@ class JniBufferManager {
}; };
struct JniCtx { struct JniCtx {
JniCtx(bool enableBufferManager) { JniCtx() { buffer_manager = new JniBufferManager(); }
if (enableBufferManager) {
buffer_manager = new JniBufferManager();
}
}
~JniCtx() { ~JniCtx() {
if (native_window) { if (native_window) {
@ -440,9 +433,8 @@ int vpx_release_frame_buffer(void* priv, vpx_codec_frame_buffer_t* fb) {
return buffer_manager->release(*(int*)fb->priv); return buffer_manager->release(*(int*)fb->priv);
} }
DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
jboolean enableBufferManager) { JniCtx* context = new JniCtx();
JniCtx* context = new JniCtx(enableBufferManager);
context->decoder = new vpx_codec_ctx_t(); context->decoder = new vpx_codec_ctx_t();
vpx_codec_dec_cfg_t cfg = {0, 0, 0}; vpx_codec_dec_cfg_t cfg = {0, 0, 0};
cfg.threads = android_getCpuCount(); cfg.threads = android_getCpuCount();
@ -469,7 +461,6 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
} }
#endif #endif
} }
if (enableBufferManager) {
err = vpx_codec_set_frame_buffer_functions( err = vpx_codec_set_frame_buffer_functions(
context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer, context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer,
context->buffer_manager); context->buffer_manager);
@ -477,15 +468,12 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.", LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.",
err); err);
} }
}
// Populate JNI References. // Populate JNI References.
const jclass outputBufferClass = env->FindClass( const jclass outputBufferClass = env->FindClass(
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer"); "com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame", initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
"(IIIII)Z"); "(IIIII)Z");
initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame",
"(II)Z");
dataField = env->GetFieldID(outputBufferClass, "data", dataField = env->GetFieldID(outputBufferClass, "data",
"Ljava/nio/ByteBuffer;"); "Ljava/nio/ByteBuffer;");
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I"); outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
@ -537,28 +525,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
} }
const int kOutputModeYuv = 0; const int kOutputModeYuv = 0;
const int kOutputModeRgb = 1; const int kOutputModeSurfaceYuv = 1;
const int kOutputModeSurfaceYuv = 2;
int outputMode = env->GetIntField(jOutputBuffer, outputModeField); int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
if (outputMode == kOutputModeRgb) { if (outputMode == kOutputModeYuv) {
// resize buffer if required.
jboolean initResult = env->CallBooleanMethod(jOutputBuffer, initForRgbFrame,
img->d_w, img->d_h);
if (env->ExceptionCheck() || !initResult) {
return -1;
}
// get pointer to the data buffer.
const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField);
uint8_t* const dst =
reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(dataObject));
libyuv::I420ToRGB565(img->planes[VPX_PLANE_Y], img->stride[VPX_PLANE_Y],
img->planes[VPX_PLANE_U], img->stride[VPX_PLANE_U],
img->planes[VPX_PLANE_V], img->stride[VPX_PLANE_V],
dst, img->d_w * 2, img->d_w, img->d_h);
} else if (outputMode == kOutputModeYuv) {
const int kColorspaceUnknown = 0; const int kColorspaceUnknown = 0;
const int kColorspaceBT601 = 1; const int kColorspaceBT601 = 1;
const int kColorspaceBT709 = 2; const int kColorspaceBT709 = 2;
@ -616,9 +586,6 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
} }
} else if (outputMode == kOutputModeSurfaceYuv && } else if (outputMode == kOutputModeSurfaceYuv &&
img->fmt != VPX_IMG_FMT_I42016) { img->fmt != VPX_IMG_FMT_I42016) {
if (!context->buffer_manager) {
return -1; // enableBufferManager was not set in vpxInit.
}
int id = *(int*)img->fb_priv; int id = *(int*)img->fb_priv;
context->buffer_manager->add_ref(id); context->buffer_manager->add_ref(id);
JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id); JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id);

View File

@ -3,7 +3,7 @@
# Constructors accessed via reflection in DefaultRenderersFactory # Constructors accessed via reflection in DefaultRenderersFactory
-dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer -dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer
-keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer { -keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer {
<init>(boolean, long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int); <init>(long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
} }
-dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer -dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer
-keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer { -keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer {
@ -44,5 +44,22 @@
<init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offlineDownloaderConstructorHelper); <init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offlineDownloaderConstructorHelper);
} }
# Constructors accessed via reflection in DownloadHelper
-dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory
-keepclassmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory {
<init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
DashMediaSource createMediaSource(android.net.Uri);
}
-dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory
-keepclassmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory {
<init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
HlsMediaSource createMediaSource(android.net.Uri);
}
-dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory
-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
<init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
SsMediaSource createMediaSource(android.net.Uri);
}
# Don't warn about checkerframework # Don't warn about checkerframework
-dontwarn org.checkerframework.** -dontwarn org.checkerframework.**

View File

@ -37,7 +37,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
private SampleStream stream; private SampleStream stream;
private Format[] streamFormats; private Format[] streamFormats;
private long streamOffsetUs; private long streamOffsetUs;
private boolean readEndOfStream; private long readingPositionUs;
private boolean streamIsFinal; private boolean streamIsFinal;
/** /**
@ -46,7 +46,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
*/ */
public BaseRenderer(int trackType) { public BaseRenderer(int trackType) {
this.trackType = trackType; this.trackType = trackType;
readEndOfStream = true; readingPositionUs = C.TIME_END_OF_SOURCE;
} }
@Override @Override
@ -98,7 +98,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
throws ExoPlaybackException { throws ExoPlaybackException {
Assertions.checkState(!streamIsFinal); Assertions.checkState(!streamIsFinal);
this.stream = stream; this.stream = stream;
readEndOfStream = false; readingPositionUs = offsetUs;
streamFormats = formats; streamFormats = formats;
streamOffsetUs = offsetUs; streamOffsetUs = offsetUs;
onStreamChanged(formats, offsetUs); onStreamChanged(formats, offsetUs);
@ -111,7 +111,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
@Override @Override
public final boolean hasReadStreamToEnd() { public final boolean hasReadStreamToEnd() {
return readEndOfStream; return readingPositionUs == C.TIME_END_OF_SOURCE;
}
@Override
public final long getReadingPositionUs() {
return readingPositionUs;
} }
@Override @Override
@ -132,7 +137,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
@Override @Override
public final void resetPosition(long positionUs) throws ExoPlaybackException { public final void resetPosition(long positionUs) throws ExoPlaybackException {
streamIsFinal = false; streamIsFinal = false;
readEndOfStream = false; readingPositionUs = positionUs;
onPositionReset(positionUs, false); onPositionReset(positionUs, false);
} }
@ -303,10 +308,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
int result = stream.readData(formatHolder, buffer, formatRequired); int result = stream.readData(formatHolder, buffer, formatRequired);
if (result == C.RESULT_BUFFER_READ) { if (result == C.RESULT_BUFFER_READ) {
if (buffer.isEndOfStream()) { if (buffer.isEndOfStream()) {
readEndOfStream = true; readingPositionUs = C.TIME_END_OF_SOURCE;
return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ; return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;
} }
buffer.timeUs += streamOffsetUs; buffer.timeUs += streamOffsetUs;
readingPositionUs = Math.max(readingPositionUs, buffer.timeUs);
} else if (result == C.RESULT_FORMAT_READ) { } else if (result == C.RESULT_FORMAT_READ) {
Format format = formatHolder.format; Format format = formatHolder.format;
if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
@ -332,7 +338,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* Returns whether the upstream source is ready. * Returns whether the upstream source is ready.
*/ */
protected final boolean isSourceReady() { protected final boolean isSourceReady() {
return readEndOfStream ? streamIsFinal : stream.isReady(); return hasReadStreamToEnd() ? streamIsFinal : stream.isReady();
} }
/** /**

View File

@ -460,8 +460,8 @@ public final class C {
/** /**
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link * Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_ENCRYPTED} and * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE},
* {@link #BUFFER_FLAG_DECODE_ONLY}. * {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@ -470,6 +470,7 @@ public final class C {
value = { value = {
BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_KEY_FRAME,
BUFFER_FLAG_END_OF_STREAM, BUFFER_FLAG_END_OF_STREAM,
BUFFER_FLAG_LAST_SAMPLE,
BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_ENCRYPTED,
BUFFER_FLAG_DECODE_ONLY BUFFER_FLAG_DECODE_ONLY
}) })
@ -482,6 +483,8 @@ public final class C {
* Flag for empty buffers that signal that the end of the stream was reached. * Flag for empty buffers that signal that the end of the stream was reached.
*/ */
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM; public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
/** Indicates that a buffer is known to contain the last media sample of the stream. */
public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000
/** Indicates that a buffer is (at least partially) encrypted. */ /** Indicates that a buffer is (at least partially) encrypted. */
public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000 public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
/** Indicates that a buffer should be decoded but not rendered. */ /** Indicates that a buffer should be decoded but not rendered. */
@ -533,9 +536,7 @@ public final class C {
*/ */
public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4 public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4
/** /** Represents an undetermined language as an ISO 639-2 language code. */
* Represents an undetermined language as an ISO 639 alpha-3 language code.
*/
public static final String LANGUAGE_UNDETERMINED = "und"; public static final String LANGUAGE_UNDETERMINED = "und";
/** /**

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.content.Context; import android.content.Context;
import android.media.MediaCodec;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
@ -85,15 +86,18 @@ public class DefaultRenderersFactory implements RenderersFactory {
protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
private final Context context; private final Context context;
private final @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager; @Nullable private DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
private final @ExtensionRendererMode int extensionRendererMode; @ExtensionRendererMode private int extensionRendererMode;
private final long allowedVideoJoiningTimeMs; private long allowedVideoJoiningTimeMs;
private boolean playClearSamplesWithoutKeys;
private MediaCodecSelector mediaCodecSelector;
/** /** @param context A {@link Context}. */
* @param context A {@link Context}.
*/
public DefaultRenderersFactory(Context context) { public DefaultRenderersFactory(Context context) {
this(context, EXTENSION_RENDERER_MODE_OFF); this.context = context;
extensionRendererMode = EXTENSION_RENDERER_MODE_OFF;
allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS;
mediaCodecSelector = MediaCodecSelector.DEFAULT;
} }
/** /**
@ -108,19 +112,20 @@ public class DefaultRenderersFactory implements RenderersFactory {
} }
/** /**
* @param context A {@link Context}. * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link
* @param extensionRendererMode The extension renderer mode, which determines if and how available * #setExtensionRendererMode(int)}.
* extension renderers are used. Note that extensions must be included in the application
* build for them to be considered available.
*/ */
@Deprecated
@SuppressWarnings("deprecation")
public DefaultRenderersFactory( public DefaultRenderersFactory(
Context context, @ExtensionRendererMode int extensionRendererMode) { Context context, @ExtensionRendererMode int extensionRendererMode) {
this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
} }
/** /**
* @deprecated Use {@link #DefaultRenderersFactory(Context, int)} and pass {@link * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link
* DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. * #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link
* SimpleExoPlayer} or {@link ExoPlayerFactory}.
*/ */
@Deprecated @Deprecated
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@ -132,26 +137,22 @@ public class DefaultRenderersFactory implements RenderersFactory {
} }
/** /**
* @param context A {@link Context}. * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link
* @param extensionRendererMode The extension renderer mode, which determines if and how available * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}.
* extension renderers are used. Note that extensions must be included in the application
* build for them to be considered available.
* @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
* seamlessly join an ongoing playback.
*/ */
@Deprecated
@SuppressWarnings("deprecation")
public DefaultRenderersFactory( public DefaultRenderersFactory(
Context context, Context context,
@ExtensionRendererMode int extensionRendererMode, @ExtensionRendererMode int extensionRendererMode,
long allowedVideoJoiningTimeMs) { long allowedVideoJoiningTimeMs) {
this.context = context; this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs);
this.extensionRendererMode = extensionRendererMode;
this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
this.drmSessionManager = null;
} }
/** /**
* @deprecated Use {@link #DefaultRenderersFactory(Context, int, long)} and pass {@link * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link
* DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}, and pass
* {@link DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}.
*/ */
@Deprecated @Deprecated
public DefaultRenderersFactory( public DefaultRenderersFactory(
@ -163,6 +164,70 @@ public class DefaultRenderersFactory implements RenderersFactory {
this.extensionRendererMode = extensionRendererMode; this.extensionRendererMode = extensionRendererMode;
this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
mediaCodecSelector = MediaCodecSelector.DEFAULT;
}
/**
* Sets the extension renderer mode, which determines if and how available extension renderers are
* used. Note that extensions must be included in the application build for them to be considered
* available.
*
* <p>The default value is {@link #EXTENSION_RENDERER_MODE_OFF}.
*
* @param extensionRendererMode The extension renderer mode.
* @return This factory, for convenience.
*/
public DefaultRenderersFactory setExtensionRendererMode(
@ExtensionRendererMode int extensionRendererMode) {
this.extensionRendererMode = extensionRendererMode;
return this;
}
/**
* Sets whether renderers are permitted to play clear regions of encrypted media prior to having
* obtained the keys necessary to decrypt encrypted regions of the media. For encrypted media that
* starts with a short clear region, this allows playback to begin in parallel with key
* acquisition, which can reduce startup latency.
*
* <p>The default value is {@code false}.
*
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
* the media.
* @return This factory, for convenience.
*/
public DefaultRenderersFactory setPlayClearSamplesWithoutKeys(
boolean playClearSamplesWithoutKeys) {
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
return this;
}
/**
* Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers.
*
* <p>The default value is {@link MediaCodecSelector#DEFAULT}.
*
* @param mediaCodecSelector The {@link MediaCodecSelector}.
* @return This factory, for convenience.
*/
public DefaultRenderersFactory setMediaCodecSelector(MediaCodecSelector mediaCodecSelector) {
this.mediaCodecSelector = mediaCodecSelector;
return this;
}
/**
* Sets the maximum duration for which video renderers can attempt to seamlessly join an ongoing
* playback.
*
* <p>The default value is {@link #DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS}.
*
* @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
* seamlessly join an ongoing playback, in milliseconds.
* @return This factory, for convenience.
*/
public DefaultRenderersFactory setAllowedVideoJoiningTimeMs(long allowedVideoJoiningTimeMs) {
this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
return this;
} }
@Override @Override
@ -177,10 +242,26 @@ public class DefaultRenderersFactory implements RenderersFactory {
drmSessionManager = this.drmSessionManager; drmSessionManager = this.drmSessionManager;
} }
ArrayList<Renderer> renderersList = new ArrayList<>(); ArrayList<Renderer> renderersList = new ArrayList<>();
buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs, buildVideoRenderers(
eventHandler, videoRendererEventListener, extensionRendererMode, renderersList); context,
buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(), extensionRendererMode,
eventHandler, audioRendererEventListener, extensionRendererMode, renderersList); mediaCodecSelector,
drmSessionManager,
playClearSamplesWithoutKeys,
eventHandler,
videoRendererEventListener,
allowedVideoJoiningTimeMs,
renderersList);
buildAudioRenderers(
context,
extensionRendererMode,
mediaCodecSelector,
drmSessionManager,
playClearSamplesWithoutKeys,
buildAudioProcessors(),
eventHandler,
audioRendererEventListener,
renderersList);
buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(), buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(),
extensionRendererMode, renderersList); extensionRendererMode, renderersList);
buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(), buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(),
@ -194,27 +275,36 @@ public class DefaultRenderersFactory implements RenderersFactory {
* Builds video renderers for use by the player. * Builds video renderers for use by the player.
* *
* @param context The {@link Context} associated with the player. * @param context The {@link Context} associated with the player.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player * @param extensionRendererMode The extension renderer mode.
* will not be used for DRM protected playbacks. * @param mediaCodecSelector A decoder selector.
* @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
* renderers can attempt to seamlessly join an ongoing playback. * not be used for DRM protected playbacks.
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
* the media.
* @param eventHandler A handler associated with the main thread's looper. * @param eventHandler A handler associated with the main thread's looper.
* @param eventListener An event listener. * @param eventListener An event listener.
* @param extensionRendererMode The extension renderer mode. * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
* seamlessly join an ongoing playback, in milliseconds.
* @param out An array to which the built renderers should be appended. * @param out An array to which the built renderers should be appended.
*/ */
protected void buildVideoRenderers(Context context, protected void buildVideoRenderers(
Context context,
@ExtensionRendererMode int extensionRendererMode,
MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
long allowedVideoJoiningTimeMs, Handler eventHandler, boolean playClearSamplesWithoutKeys,
VideoRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, Handler eventHandler,
VideoRendererEventListener eventListener,
long allowedVideoJoiningTimeMs,
ArrayList<Renderer> out) { ArrayList<Renderer> out) {
out.add( out.add(
new MediaCodecVideoRenderer( new MediaCodecVideoRenderer(
context, context,
MediaCodecSelector.DEFAULT, mediaCodecSelector,
allowedVideoJoiningTimeMs, allowedVideoJoiningTimeMs,
drmSessionManager, drmSessionManager,
/* playClearSamplesWithoutKeys= */ false, playClearSamplesWithoutKeys,
eventHandler, eventHandler,
eventListener, eventListener,
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
@ -233,7 +323,6 @@ public class DefaultRenderersFactory implements RenderersFactory {
Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer"); Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
Constructor<?> constructor = Constructor<?> constructor =
clazz.getConstructor( clazz.getConstructor(
boolean.class,
long.class, long.class,
android.os.Handler.class, android.os.Handler.class,
com.google.android.exoplayer2.video.VideoRendererEventListener.class, com.google.android.exoplayer2.video.VideoRendererEventListener.class,
@ -242,7 +331,6 @@ public class DefaultRenderersFactory implements RenderersFactory {
Renderer renderer = Renderer renderer =
(Renderer) (Renderer)
constructor.newInstance( constructor.newInstance(
true,
allowedVideoJoiningTimeMs, allowedVideoJoiningTimeMs,
eventHandler, eventHandler,
eventListener, eventListener,
@ -261,26 +349,35 @@ public class DefaultRenderersFactory implements RenderersFactory {
* Builds audio renderers for use by the player. * Builds audio renderers for use by the player.
* *
* @param context The {@link Context} associated with the player. * @param context The {@link Context} associated with the player.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player * @param extensionRendererMode The extension renderer mode.
* will not be used for DRM protected playbacks. * @param mediaCodecSelector A decoder selector.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
* buffers before output. May be empty. * not be used for DRM protected playbacks.
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
* the media.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers
* before output. May be empty.
* @param eventHandler A handler to use when invoking event listeners and outputs. * @param eventHandler A handler to use when invoking event listeners and outputs.
* @param eventListener An event listener. * @param eventListener An event listener.
* @param extensionRendererMode The extension renderer mode.
* @param out An array to which the built renderers should be appended. * @param out An array to which the built renderers should be appended.
*/ */
protected void buildAudioRenderers(Context context, protected void buildAudioRenderers(
Context context,
@ExtensionRendererMode int extensionRendererMode,
MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
AudioProcessor[] audioProcessors, Handler eventHandler, boolean playClearSamplesWithoutKeys,
AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, AudioProcessor[] audioProcessors,
Handler eventHandler,
AudioRendererEventListener eventListener,
ArrayList<Renderer> out) { ArrayList<Renderer> out) {
out.add( out.add(
new MediaCodecAudioRenderer( new MediaCodecAudioRenderer(
context, context,
MediaCodecSelector.DEFAULT, mediaCodecSelector,
drmSessionManager, drmSessionManager,
/* playClearSamplesWithoutKeys= */ false, playClearSamplesWithoutKeys,
eventHandler, eventHandler,
eventListener, eventListener,
AudioCapabilities.getCapabilities(context), AudioCapabilities.getCapabilities(context),

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
@ -34,7 +35,7 @@ public final class ExoPlaybackException extends Exception {
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED}) @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED, TYPE_REMOTE})
public @interface Type {} public @interface Type {}
/** /**
* The error occurred loading data from a {@link MediaSource}. * The error occurred loading data from a {@link MediaSource}.
@ -54,6 +55,12 @@ public final class ExoPlaybackException extends Exception {
* Call {@link #getUnexpectedException()} to retrieve the underlying cause. * Call {@link #getUnexpectedException()} to retrieve the underlying cause.
*/ */
public static final int TYPE_UNEXPECTED = 2; public static final int TYPE_UNEXPECTED = 2;
/**
* The error occurred in a remote component.
*
* <p>Call {@link #getMessage()} to retrieve the message associated with the error.
*/
public static final int TYPE_REMOTE = 3;
/** /**
* The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and * The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
@ -66,7 +73,7 @@ public final class ExoPlaybackException extends Exception {
*/ */
public final int rendererIndex; public final int rendererIndex;
private final Throwable cause; @Nullable private final Throwable cause;
/** /**
* Creates an instance of type {@link #TYPE_SOURCE}. * Creates an instance of type {@link #TYPE_SOURCE}.
@ -99,6 +106,16 @@ public final class ExoPlaybackException extends Exception {
return new ExoPlaybackException(TYPE_UNEXPECTED, cause, C.INDEX_UNSET); return new ExoPlaybackException(TYPE_UNEXPECTED, cause, C.INDEX_UNSET);
} }
/**
* Creates an instance of type {@link #TYPE_REMOTE}.
*
* @param message The message associated with the error.
* @return The created instance.
*/
public static ExoPlaybackException createForRemote(String message) {
return new ExoPlaybackException(TYPE_REMOTE, message);
}
private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) { private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) {
super(cause); super(cause);
this.type = type; this.type = type;
@ -106,6 +123,13 @@ public final class ExoPlaybackException extends Exception {
this.rendererIndex = rendererIndex; this.rendererIndex = rendererIndex;
} }
private ExoPlaybackException(@Type int type, String message) {
super(message);
this.type = type;
rendererIndex = C.INDEX_UNSET;
cause = null;
}
/** /**
* Retrieves the underlying error when {@link #type} is {@link #TYPE_SOURCE}. * Retrieves the underlying error when {@link #type} is {@link #TYPE_SOURCE}.
* *
@ -113,7 +137,7 @@ public final class ExoPlaybackException extends Exception {
*/ */
public IOException getSourceException() { public IOException getSourceException() {
Assertions.checkState(type == TYPE_SOURCE); Assertions.checkState(type == TYPE_SOURCE);
return (IOException) cause; return (IOException) Assertions.checkNotNull(cause);
} }
/** /**
@ -123,7 +147,7 @@ public final class ExoPlaybackException extends Exception {
*/ */
public Exception getRendererException() { public Exception getRendererException() {
Assertions.checkState(type == TYPE_RENDERER); Assertions.checkState(type == TYPE_RENDERER);
return (Exception) cause; return (Exception) Assertions.checkNotNull(cause);
} }
/** /**
@ -133,7 +157,7 @@ public final class ExoPlaybackException extends Exception {
*/ */
public RuntimeException getUnexpectedException() { public RuntimeException getUnexpectedException() {
Assertions.checkState(type == TYPE_UNEXPECTED); Assertions.checkState(type == TYPE_UNEXPECTED);
return (RuntimeException) cause; return (RuntimeException) Assertions.checkNotNull(cause);
} }
} }

View File

@ -21,10 +21,10 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ClippingMediaSource;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
@ -48,7 +48,7 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* <li>A <b>{@link MediaSource}</b> that defines the media to be played, loads the media, and from * <li>A <b>{@link MediaSource}</b> that defines the media to be played, loads the media, and from
* which the loaded media can be read. A MediaSource is injected via {@link * which the loaded media can be read. A MediaSource is injected via {@link
* #prepare(MediaSource)} at the start of playback. The library modules provide default * #prepare(MediaSource)} at the start of playback. The library modules provide default
* implementations for regular media files ({@link ExtractorMediaSource}), DASH * implementations for progressive media files ({@link ProgressiveMediaSource}), DASH
* (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an * (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an
* implementation for loading single media samples ({@link SingleSampleMediaSource}) that's * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's
* most often used for side-loaded subtitle files, and implementations for building more * most often used for side-loaded subtitle files, and implementations for building more

View File

@ -58,7 +58,8 @@ public final class ExoPlayerFactory {
LoadControl loadControl, LoadControl loadControl,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) { @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode); RenderersFactory renderersFactory =
new DefaultRenderersFactory(context).setExtensionRendererMode(extensionRendererMode);
return newSimpleInstance( return newSimpleInstance(
context, renderersFactory, trackSelector, loadControl, drmSessionManager); context, renderersFactory, trackSelector, loadControl, drmSessionManager);
} }
@ -88,7 +89,9 @@ public final class ExoPlayerFactory {
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
long allowedVideoJoiningTimeMs) { long allowedVideoJoiningTimeMs) {
RenderersFactory renderersFactory = RenderersFactory renderersFactory =
new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs); new DefaultRenderersFactory(context)
.setExtensionRendererMode(extensionRendererMode)
.setAllowedVideoJoiningTimeMs(allowedVideoJoiningTimeMs);
return newSimpleInstance( return newSimpleInstance(
context, renderersFactory, trackSelector, loadControl, drmSessionManager); context, renderersFactory, trackSelector, loadControl, drmSessionManager);
} }

View File

@ -1376,12 +1376,34 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
if (!queue.updateQueuedPeriods(playingPeriodId, rendererPositionUs)) { if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) {
seekToCurrentPosition(/* sendDiscontinuity= */ false); seekToCurrentPosition(/* sendDiscontinuity= */ false);
} }
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
} }
private long getMaxRendererReadPositionUs() {
MediaPeriodHolder readingHolder = queue.getReadingPeriod();
if (readingHolder == null) {
return 0;
}
long maxReadPositionUs = readingHolder.getRendererOffset();
for (int i = 0; i < renderers.length; i++) {
if (renderers[i].getState() == Renderer.STATE_DISABLED
|| renderers[i].getStream() != readingHolder.sampleStreams[i]) {
// Ignore disabled renderers and renderers with sample streams from previous periods.
continue;
}
long readingPositionUs = renderers[i].getReadingPositionUs();
if (readingPositionUs == C.TIME_END_OF_SOURCE) {
return C.TIME_END_OF_SOURCE;
} else {
maxReadPositionUs = Math.max(readingPositionUs, maxReadPositionUs);
}
}
return maxReadPositionUs;
}
private void handleSourceInfoRefreshEndedPlayback() { private void handleSourceInfoRefreshEndedPlayback() {
setState(Player.STATE_ENDED); setState(Player.STATE_ENDED);
// Reset, but retain the source so that it can still be used should a seek occur. // Reset, but retain the source so that it can still be used should a seek occur.

View File

@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.9.3"; public static final String VERSION = "2.9.5";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.3"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.5";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2009003; public static final int VERSION_INT = 2009005;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View File

@ -159,7 +159,7 @@ public final class Format implements Parcelable {
@C.SelectionFlags @C.SelectionFlags
public final int selectionFlags; public final int selectionFlags;
/** The language, or null if unknown or not applicable. */ /** The language as ISO 639-2/T three-letter code, or null if unknown or not applicable. */
public final @Nullable String language; public final @Nullable String language;
/** /**
@ -932,7 +932,7 @@ public final class Format implements Parcelable {
this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay; this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay;
this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding; this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding;
this.selectionFlags = selectionFlags; this.selectionFlags = selectionFlags;
this.language = language; this.language = Util.normalizeLanguageCode(language);
this.accessibilityChannel = accessibilityChannel; this.accessibilityChannel = accessibilityChannel;
this.subsampleOffsetUs = subsampleOffsetUs; this.subsampleOffsetUs = subsampleOffsetUs;
this.initializationData = this.initializationData =

View File

@ -89,7 +89,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
this.info = info; this.info = info;
sampleStreams = new SampleStream[rendererCapabilities.length]; sampleStreams = new SampleStream[rendererCapabilities.length];
mayRetainStreamFlags = new boolean[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length];
mediaPeriod = createMediaPeriod(info.id, mediaSource, allocator); mediaPeriod =
createMediaPeriod(
info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs);
} }
/** /**
@ -294,7 +296,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public void release() { public void release() {
disableTrackSelectionsInResult(); disableTrackSelectionsInResult();
trackSelectorResult = null; trackSelectorResult = null;
releaseMediaPeriod(info.id, mediaSource, mediaPeriod); releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod);
} }
/** /**
@ -399,24 +401,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Returns a media period corresponding to the given {@code id}. */ /** Returns a media period corresponding to the given {@code id}. */
private static MediaPeriod createMediaPeriod( private static MediaPeriod createMediaPeriod(
MediaPeriodId id, MediaSource mediaSource, Allocator allocator) { MediaPeriodId id,
MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator); MediaSource mediaSource,
if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) { Allocator allocator,
long startPositionUs,
long endPositionUs) {
MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs);
if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
mediaPeriod = mediaPeriod =
new ClippingMediaPeriod( new ClippingMediaPeriod(
mediaPeriod, mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, endPositionUs);
/* enableInitialDiscontinuity= */ true,
/* startUs= */ 0,
id.endPositionUs);
} }
return mediaPeriod; return mediaPeriod;
} }
/** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */ /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */
private static void releaseMediaPeriod( private static void releaseMediaPeriod(
MediaPeriodId id, MediaSource mediaSource, MediaPeriod mediaPeriod) { long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) {
try { try {
if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) { if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
} else { } else {
mediaSource.releasePeriod(mediaPeriod); mediaSource.releasePeriod(mediaPeriod);

View File

@ -33,7 +33,14 @@ import com.google.android.exoplayer2.util.Util;
*/ */
public final long contentPositionUs; public final long contentPositionUs;
/** /**
* The duration of the media period, like {@link MediaPeriodId#endPositionUs} but with {@link * The end position to which the media period's content is clipped in order to play a following ad
* group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if this
* media period is an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll ad
* follows at the end of this content media period.
*/
public final long endPositionUs;
/**
* The duration of the media period, like {@link #endPositionUs} but with {@link
* C#TIME_END_OF_SOURCE} and {@link C#TIME_UNSET} resolved to the timeline period duration if * C#TIME_END_OF_SOURCE} and {@link C#TIME_UNSET} resolved to the timeline period duration if
* known. * known.
*/ */
@ -53,23 +60,48 @@ import com.google.android.exoplayer2.util.Util;
MediaPeriodId id, MediaPeriodId id,
long startPositionUs, long startPositionUs,
long contentPositionUs, long contentPositionUs,
long endPositionUs,
long durationUs, long durationUs,
boolean isLastInTimelinePeriod, boolean isLastInTimelinePeriod,
boolean isFinal) { boolean isFinal) {
this.id = id; this.id = id;
this.startPositionUs = startPositionUs; this.startPositionUs = startPositionUs;
this.contentPositionUs = contentPositionUs; this.contentPositionUs = contentPositionUs;
this.endPositionUs = endPositionUs;
this.durationUs = durationUs; this.durationUs = durationUs;
this.isLastInTimelinePeriod = isLastInTimelinePeriod; this.isLastInTimelinePeriod = isLastInTimelinePeriod;
this.isFinal = isFinal; this.isFinal = isFinal;
} }
/** Returns a copy of this instance with the start position set to the specified value. */ /**
* Returns a copy of this instance with the start position set to the specified value. May return
* the same instance if nothing changed.
*/
public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) { public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) {
return new MediaPeriodInfo( return startPositionUs == this.startPositionUs
? this
: new MediaPeriodInfo(
id, id,
startPositionUs, startPositionUs,
contentPositionUs, contentPositionUs,
endPositionUs,
durationUs,
isLastInTimelinePeriod,
isFinal);
}
/**
* Returns a copy of this instance with the content position set to the specified value. May
* return the same instance if nothing changed.
*/
public MediaPeriodInfo copyWithContentPositionUs(long contentPositionUs) {
return contentPositionUs == this.contentPositionUs
? this
: new MediaPeriodInfo(
id,
startPositionUs,
contentPositionUs,
endPositionUs,
durationUs, durationUs,
isLastInTimelinePeriod, isLastInTimelinePeriod,
isFinal); isFinal);
@ -86,6 +118,7 @@ import com.google.android.exoplayer2.util.Util;
MediaPeriodInfo that = (MediaPeriodInfo) o; MediaPeriodInfo that = (MediaPeriodInfo) o;
return startPositionUs == that.startPositionUs return startPositionUs == that.startPositionUs
&& contentPositionUs == that.contentPositionUs && contentPositionUs == that.contentPositionUs
&& endPositionUs == that.endPositionUs
&& durationUs == that.durationUs && durationUs == that.durationUs
&& isLastInTimelinePeriod == that.isLastInTimelinePeriod && isLastInTimelinePeriod == that.isLastInTimelinePeriod
&& isFinal == that.isFinal && isFinal == that.isFinal
@ -98,6 +131,7 @@ import com.google.android.exoplayer2.util.Util;
result = 31 * result + id.hashCode(); result = 31 * result + id.hashCode();
result = 31 * result + (int) startPositionUs; result = 31 * result + (int) startPositionUs;
result = 31 * result + (int) contentPositionUs; result = 31 * result + (int) contentPositionUs;
result = 31 * result + (int) endPositionUs;
result = 31 * result + (int) durationUs; result = 31 * result + (int) durationUs;
result = 31 * result + (isLastInTimelinePeriod ? 1 : 0); result = 31 * result + (isLastInTimelinePeriod ? 1 : 0);
result = 31 * result + (isFinal ? 1 : 0); result = 31 * result + (isFinal ? 1 : 0);

View File

@ -61,8 +61,8 @@ import com.google.android.exoplayer2.util.Assertions;
} }
/** /**
* Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(MediaPeriodId, long)} to update the * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long, long)} to update the queued
* queued media periods to take into account the new timeline. * media periods to take into account the new timeline.
*/ */
public void setTimeline(Timeline timeline) { public void setTimeline(Timeline timeline) {
this.timeline = timeline; this.timeline = timeline;
@ -292,54 +292,56 @@ import com.google.android.exoplayer2.util.Assertions;
* current playback position. The method assumes that the first media period in the queue is still * current playback position. The method assumes that the first media period in the queue is still
* consistent with the new timeline. * consistent with the new timeline.
* *
* @param playingPeriodId The current playing media period identifier.
* @param rendererPositionUs The current renderer position in microseconds. * @param rendererPositionUs The current renderer position in microseconds.
* @param maxRendererReadPositionUs The maximum renderer position up to which renderers have read
* the current reading media period in microseconds, or {@link C#TIME_END_OF_SOURCE} if they
* have read to the end.
* @return Whether the timeline change has been handled completely. * @return Whether the timeline change has been handled completely.
*/ */
public boolean updateQueuedPeriods(MediaPeriodId playingPeriodId, long rendererPositionUs) { public boolean updateQueuedPeriods(long rendererPositionUs, long maxRendererReadPositionUs) {
// TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline
// is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be
// handled here. // handled here.
int periodIndex = timeline.getIndexOfPeriod(playingPeriodId.periodUid);
// The front period is either playing now, or is being loaded and will become the playing
// period.
MediaPeriodHolder previousPeriodHolder = null; MediaPeriodHolder previousPeriodHolder = null;
MediaPeriodHolder periodHolder = getFrontPeriod(); MediaPeriodHolder periodHolder = getFrontPeriod();
while (periodHolder != null) { while (periodHolder != null) {
MediaPeriodInfo oldPeriodInfo = periodHolder.info;
// Get period info based on new timeline.
MediaPeriodInfo newPeriodInfo;
if (previousPeriodHolder == null) { if (previousPeriodHolder == null) {
long previousDurationUs = periodHolder.info.durationUs; // The id and start position of the first period have already been verified by
periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info); // ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline
if (!canKeepAfterMediaPeriodHolder(periodHolder, previousDurationUs)) { // and isLastInPeriod flags.
return !removeAfter(periodHolder); newPeriodInfo = getUpdatedMediaPeriodInfo(oldPeriodInfo);
}
} else { } else {
// Check this period holder still follows the previous one, based on the new timeline. newPeriodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);
if (periodIndex == C.INDEX_UNSET if (newPeriodInfo == null) {
|| !periodHolder.uid.equals(timeline.getUidOfPeriod(periodIndex))) {
// The holder uid is inconsistent with the new timeline.
return !removeAfter(previousPeriodHolder);
}
MediaPeriodInfo periodInfo =
getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);
if (periodInfo == null) {
// We've loaded a next media period that is not in the new timeline. // We've loaded a next media period that is not in the new timeline.
return !removeAfter(previousPeriodHolder); return !removeAfter(previousPeriodHolder);
} }
// Update the period holder. if (!canKeepMediaPeriodHolder(oldPeriodInfo, newPeriodInfo)) {
periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info); // The new media period has a different id or start position.
// Check the media period information matches the new timeline.
if (!canKeepMediaPeriodHolder(periodHolder, periodInfo)) {
return !removeAfter(previousPeriodHolder); return !removeAfter(previousPeriodHolder);
} else if (!canKeepAfterMediaPeriodHolder(periodHolder, periodInfo.durationUs)) {
return !removeAfter(periodHolder);
} }
} }
if (periodHolder.info.isLastInTimelinePeriod) { // Use new period info, but keep old content position.
// Move on to the next timeline period index, if there is one. periodHolder.info = newPeriodInfo.copyWithContentPositionUs(oldPeriodInfo.contentPositionUs);
periodIndex =
timeline.getNextPeriodIndex( if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) {
periodIndex, period, window, repeatMode, shuffleModeEnabled); // The period duration changed. Remove all subsequent periods and check whether we read
// beyond the new duration.
long newDurationInRendererTime =
newPeriodInfo.durationUs == C.TIME_UNSET
? Long.MAX_VALUE
: periodHolder.toRendererTime(newPeriodInfo.durationUs);
boolean isReadingAndReadBeyondNewDuration =
periodHolder == reading
&& (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE
|| maxRendererReadPositionUs >= newDurationInRendererTime);
boolean readingPeriodRemoved = removeAfter(periodHolder);
return !readingPeriodRemoved && !isReadingAndReadBeyondNewDuration;
} }
previousPeriodHolder = periodHolder; previousPeriodHolder = periodHolder;
@ -364,13 +366,14 @@ import com.google.android.exoplayer2.util.Assertions;
long durationUs = long durationUs =
id.isAd() id.isAd()
? period.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup) ? period.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup)
: (id.endPositionUs == C.TIME_UNSET || id.endPositionUs == C.TIME_END_OF_SOURCE : (info.endPositionUs == C.TIME_UNSET || info.endPositionUs == C.TIME_END_OF_SOURCE
? period.getDurationUs() ? period.getDurationUs()
: id.endPositionUs); : info.endPositionUs);
return new MediaPeriodInfo( return new MediaPeriodInfo(
id, id,
info.startPositionUs, info.startPositionUs,
info.contentPositionUs, info.contentPositionUs,
info.endPositionUs,
durationUs, durationUs,
isLastInPeriod, isLastInPeriod,
isLastInTimeline); isLastInTimeline);
@ -409,11 +412,7 @@ import com.google.android.exoplayer2.util.Assertions;
int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
if (adGroupIndex == C.INDEX_UNSET) { if (adGroupIndex == C.INDEX_UNSET) {
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs); int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
long endPositionUs = return new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);
nextAdGroupIndex == C.INDEX_UNSET
? C.TIME_UNSET
: period.getAdGroupTimeUs(nextAdGroupIndex);
return new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs);
} else { } else {
int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex); int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex);
return new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); return new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
@ -465,22 +464,18 @@ import com.google.android.exoplayer2.util.Assertions;
} }
/** /**
* Returns whether {@code periodHolder} can be kept for playing the media period described by * Returns whether a period described by {@code oldInfo} can be kept for playing the media period
* {@code info}. * described by {@code newInfo}.
*/ */
private boolean canKeepMediaPeriodHolder(MediaPeriodHolder periodHolder, MediaPeriodInfo info) { private boolean canKeepMediaPeriodHolder(MediaPeriodInfo oldInfo, MediaPeriodInfo newInfo) {
MediaPeriodInfo periodHolderInfo = periodHolder.info; return oldInfo.startPositionUs == newInfo.startPositionUs && oldInfo.id.equals(newInfo.id);
return periodHolderInfo.startPositionUs == info.startPositionUs
&& periodHolderInfo.id.equals(info.id);
} }
/** /**
* Returns whether periods after {@code periodHolder} can be kept for playing given its previous * Returns whether a duration change of a period is compatible with keeping the following periods.
* duration.
*/ */
private boolean canKeepAfterMediaPeriodHolder( private boolean areDurationsCompatible(long previousDurationUs, long newDurationUs) {
MediaPeriodHolder periodHolder, long previousDurationUs) { return previousDurationUs == C.TIME_UNSET || previousDurationUs == newDurationUs;
return previousDurationUs == C.TIME_UNSET || previousDurationUs == periodHolder.info.durationUs;
} }
/** /**
@ -645,7 +640,7 @@ import com.google.android.exoplayer2.util.Assertions;
} }
} else { } else {
// Play the next ad group if it's available. // Play the next ad group if it's available.
int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.id.endPositionUs); int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs);
if (nextAdGroupIndex == C.INDEX_UNSET) { if (nextAdGroupIndex == C.INDEX_UNSET) {
// The next ad group can't be played. Play content from the previous end position instead. // The next ad group can't be played. Play content from the previous end position instead.
return getMediaPeriodInfoForContent( return getMediaPeriodInfoForContent(
@ -703,6 +698,7 @@ import com.google.android.exoplayer2.util.Assertions;
id, id,
startPositionUs, startPositionUs,
contentPositionUs, contentPositionUs,
/* endPositionUs= */ C.TIME_UNSET,
durationUs, durationUs,
/* isLastInTimelinePeriod= */ false, /* isLastInTimelinePeriod= */ false,
/* isFinal= */ false); /* isFinal= */ false);
@ -711,13 +707,13 @@ import com.google.android.exoplayer2.util.Assertions;
private MediaPeriodInfo getMediaPeriodInfoForContent( private MediaPeriodInfo getMediaPeriodInfoForContent(
Object periodUid, long startPositionUs, long windowSequenceNumber) { Object periodUid, long startPositionUs, long windowSequenceNumber) {
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);
boolean isLastInPeriod = isLastInPeriod(id);
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
long endPositionUs = long endPositionUs =
nextAdGroupIndex != C.INDEX_UNSET nextAdGroupIndex != C.INDEX_UNSET
? period.getAdGroupTimeUs(nextAdGroupIndex) ? period.getAdGroupTimeUs(nextAdGroupIndex)
: C.TIME_UNSET; : C.TIME_UNSET;
MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs);
boolean isLastInPeriod = isLastInPeriod(id);
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
long durationUs = long durationUs =
endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE
? period.durationUs ? period.durationUs
@ -726,13 +722,14 @@ import com.google.android.exoplayer2.util.Assertions;
id, id,
startPositionUs, startPositionUs,
/* contentPositionUs= */ C.TIME_UNSET, /* contentPositionUs= */ C.TIME_UNSET,
endPositionUs,
durationUs, durationUs,
isLastInPeriod, isLastInPeriod,
isLastInTimeline); isLastInTimeline);
} }
private boolean isLastInPeriod(MediaPeriodId id) { private boolean isLastInPeriod(MediaPeriodId id) {
return !id.isAd() && id.endPositionUs == C.TIME_UNSET; return !id.isAd() && id.nextAdGroupIndex == C.INDEX_UNSET;
} }
private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {

View File

@ -122,6 +122,11 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
return true; return true;
} }
@Override
public long getReadingPositionUs() {
return C.TIME_END_OF_SOURCE;
}
@Override @Override
public final void setCurrentStreamFinal() { public final void setCurrentStreamFinal() {
streamIsFinal = true; streamIsFinal = true;

View File

@ -160,6 +160,16 @@ public interface Renderer extends PlayerMessage.Target {
*/ */
boolean hasReadStreamToEnd(); boolean hasReadStreamToEnd();
/**
* Returns the playback position up to which the renderer has read samples from the current {@link
* SampleStream}, in microseconds, or {@link C#TIME_END_OF_SOURCE} if the renderer has read the
* current {@link SampleStream} to the end.
*
* <p>This method may be called when the renderer is in the following states: {@link
* #STATE_ENABLED}, {@link #STATE_STARTED}.
*/
long getReadingPositionUs();
/** /**
* Signals to the renderer that the current {@link SampleStream} will be the final one supplied * Signals to the renderer that the current {@link SampleStream} will be the final one supplied
* before it is next disabled or reset. * before it is next disabled or reset.

View File

@ -63,7 +63,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
* An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can
* be obtained from {@link ExoPlayerFactory}. * be obtained from {@link ExoPlayerFactory}.
*/ */
@TargetApi(16)
public class SimpleExoPlayer extends BasePlayer public class SimpleExoPlayer extends BasePlayer
implements ExoPlayer, implements ExoPlayer,
Player.AudioComponent, Player.AudioComponent,
@ -94,25 +93,25 @@ public class SimpleExoPlayer extends BasePlayer
private final AudioFocusManager audioFocusManager; private final AudioFocusManager audioFocusManager;
private Format videoFormat; @Nullable private Format videoFormat;
private Format audioFormat; @Nullable private Format audioFormat;
private Surface surface; @Nullable private Surface surface;
private boolean ownsSurface; private boolean ownsSurface;
private @C.VideoScalingMode int videoScalingMode; private @C.VideoScalingMode int videoScalingMode;
private SurfaceHolder surfaceHolder; @Nullable private SurfaceHolder surfaceHolder;
private TextureView textureView; @Nullable private TextureView textureView;
private int surfaceWidth; private int surfaceWidth;
private int surfaceHeight; private int surfaceHeight;
private DecoderCounters videoDecoderCounters; @Nullable private DecoderCounters videoDecoderCounters;
private DecoderCounters audioDecoderCounters; @Nullable private DecoderCounters audioDecoderCounters;
private int audioSessionId; private int audioSessionId;
private AudioAttributes audioAttributes; private AudioAttributes audioAttributes;
private float audioVolume; private float audioVolume;
private MediaSource mediaSource; @Nullable private MediaSource mediaSource;
private List<Cue> currentCues; private List<Cue> currentCues;
private VideoFrameMetadataListener videoFrameMetadataListener; @Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
private CameraMotionListener cameraMotionListener; @Nullable private CameraMotionListener cameraMotionListener;
private boolean hasNotifiedFullWrongThreadWarning; private boolean hasNotifiedFullWrongThreadWarning;
/** /**
@ -558,30 +557,26 @@ public class SimpleExoPlayer extends BasePlayer
setPlaybackParameters(playbackParameters); setPlaybackParameters(playbackParameters);
} }
/** /** Returns the video format currently being played, or null if no video is being played. */
* Returns the video format currently being played, or null if no video is being played. @Nullable
*/
public Format getVideoFormat() { public Format getVideoFormat() {
return videoFormat; return videoFormat;
} }
/** /** Returns the audio format currently being played, or null if no audio is being played. */
* Returns the audio format currently being played, or null if no audio is being played. @Nullable
*/
public Format getAudioFormat() { public Format getAudioFormat() {
return audioFormat; return audioFormat;
} }
/** /** Returns {@link DecoderCounters} for video, or null if no video is being played. */
* Returns {@link DecoderCounters} for video, or null if no video is being played. @Nullable
*/
public DecoderCounters getVideoDecoderCounters() { public DecoderCounters getVideoDecoderCounters() {
return videoDecoderCounters; return videoDecoderCounters;
} }
/** /** Returns {@link DecoderCounters} for audio, or null if no audio is being played. */
* Returns {@link DecoderCounters} for audio, or null if no audio is being played. @Nullable
*/
public DecoderCounters getAudioDecoderCounters() { public DecoderCounters getAudioDecoderCounters() {
return audioDecoderCounters; return audioDecoderCounters;
} }
@ -1053,7 +1048,8 @@ public class SimpleExoPlayer extends BasePlayer
} }
@Override @Override
public @Nullable Object getCurrentManifest() { @Nullable
public Object getCurrentManifest() {
verifyApplicationThread(); verifyApplicationThread();
return player.getCurrentManifest(); return player.getCurrentManifest();
} }

View File

@ -129,12 +129,13 @@ public class AnalyticsCollector
/** /**
* Sets the player for which data will be collected. Must only be called if no player has been set * Sets the player for which data will be collected. Must only be called if no player has been set
* yet. * yet or the current player is idle.
* *
* @param player The {@link Player} for which data will be collected. * @param player The {@link Player} for which data will be collected.
*/ */
public void setPlayer(Player player) { public void setPlayer(Player player) {
Assertions.checkState(this.player == null); Assertions.checkState(
this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty());
this.player = Assertions.checkNotNull(player); this.player = Assertions.checkNotNull(player);
} }
@ -488,7 +489,10 @@ public class AnalyticsCollector
@Override @Override
public final void onPlayerError(ExoPlaybackException error) { public final void onPlayerError(ExoPlaybackException error) {
EventTime eventTime = generatePlayingMediaPeriodEventTime(); EventTime eventTime =
error.type == ExoPlaybackException.TYPE_SOURCE
? generateLoadingMediaPeriodEventTime()
: generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
listener.onPlayerError(eventTime, error); listener.onPlayerError(eventTime, error);
} }

View File

@ -147,6 +147,7 @@ public interface AudioRendererEventListener {
* Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}. * Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
*/ */
public void disabled(final DecoderCounters counters) { public void disabled(final DecoderCounters counters) {
counters.ensureUpdated();
if (listener != null) { if (listener != null) {
handler.post( handler.post(
() -> { () -> {

View File

@ -418,7 +418,7 @@ public final class DefaultAudioSink implements AudioSink {
isInputPcm = Util.isEncodingLinearPcm(inputEncoding); isInputPcm = Util.isEncodingLinearPcm(inputEncoding);
shouldConvertHighResIntPcmToFloat = shouldConvertHighResIntPcmToFloat =
enableConvertHighResIntPcmToFloat enableConvertHighResIntPcmToFloat
&& supportsOutput(channelCount, C.ENCODING_PCM_32BIT) && supportsOutput(channelCount, C.ENCODING_PCM_FLOAT)
&& Util.isEncodingHighResolutionIntegerPcm(inputEncoding); && Util.isEncodingHighResolutionIntegerPcm(inputEncoding);
if (isInputPcm) { if (isInputPcm) {
pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount);

View File

@ -16,7 +16,6 @@
package com.google.android.exoplayer2.audio; package com.google.android.exoplayer2.audio;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCrypto; import android.media.MediaCrypto;
@ -66,7 +65,6 @@ import java.util.List;
* underlying audio track. * underlying audio track.
* </ul> * </ul>
*/ */
@TargetApi(16)
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock { public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
/** /**
@ -548,7 +546,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
try { try {
super.onDisabled(); super.onDisabled();
} finally { } finally {
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters); eventDispatcher.disabled(decoderCounters);
} }
} }

View File

@ -106,8 +106,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
? extends AudioDecoderException> decoder; ? extends AudioDecoderException> decoder;
private DecoderInputBuffer inputBuffer; private DecoderInputBuffer inputBuffer;
private SimpleOutputBuffer outputBuffer; private SimpleOutputBuffer outputBuffer;
private DrmSession<ExoMediaCrypto> drmSession; @Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
private DrmSession<ExoMediaCrypto> pendingDrmSession; @Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;
@ReinitializationState private int decoderReinitializationState; @ReinitializationState private int decoderReinitializationState;
private boolean decoderReceivedBuffers; private boolean decoderReceivedBuffers;
@ -462,12 +462,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} }
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false; return false;
} }
@DrmSession.State int drmSessionState = drmSession.getState(); @DrmSession.State int drmSessionState = decoderDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) { if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
} }
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
} }
@ -568,27 +568,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
audioTrackNeedsConfigure = true; audioTrackNeedsConfigure = true;
waitingForKeys = false; waitingForKeys = false;
try { try {
setSourceDrmSession(null);
releaseDecoder(); releaseDecoder();
audioSink.reset(); audioSink.reset();
} finally { } finally {
try {
if (drmSession != null) {
drmSessionManager.releaseSession(drmSession);
}
} finally {
try {
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} finally {
drmSession = null;
pendingDrmSession = null;
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters); eventDispatcher.disabled(decoderCounters);
} }
} }
}
}
@Override @Override
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
@ -615,12 +601,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return; return;
} }
drmSession = pendingDrmSession; setDecoderDrmSession(sourceDrmSession);
ExoMediaCrypto mediaCrypto = null; ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) { if (decoderDrmSession != null) {
mediaCrypto = drmSession.getMediaCrypto(); mediaCrypto = decoderDrmSession.getMediaCrypto();
if (mediaCrypto == null) { if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError(); DrmSessionException drmError = decoderDrmSession.getError();
if (drmError != null) { if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a new // Continue for now. We may be able to avoid failure if the session recovers, or if a new
// input format causes the session to be replaced before it's used. // input format causes the session to be replaced before it's used.
@ -646,17 +633,34 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} }
private void releaseDecoder() { private void releaseDecoder() {
if (decoder == null) {
return;
}
inputBuffer = null; inputBuffer = null;
outputBuffer = null; outputBuffer = null;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false;
if (decoder != null) {
decoder.release(); decoder.release();
decoder = null; decoder = null;
decoderCounters.decoderReleaseCount++; decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE; }
decoderReceivedBuffers = false; setDecoderDrmSession(null);
}
private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
DrmSession<ExoMediaCrypto> previous = sourceDrmSession;
sourceDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
DrmSession<ExoMediaCrypto> previous = decoderDrmSession;
decoderDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void releaseDrmSessionIfUnused(@Nullable DrmSession<ExoMediaCrypto> session) {
if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
drmSessionManager.releaseSession(session);
}
} }
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
@ -671,13 +675,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
throw ExoPlaybackException.createForRenderer( throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
} }
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), DrmSession<ExoMediaCrypto> session =
inputFormat.drmInitData); drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
if (pendingDrmSession == drmSession) { if (session == decoderDrmSession || session == sourceDrmSession) {
drmSessionManager.releaseSession(pendingDrmSession); // We already had this session. The manager must be reference counting, so release it once
// to get the count attributed to this renderer back down to 1.
drmSessionManager.releaseSession(session);
} }
setSourceDrmSession(session);
} else { } else {
pendingDrmSession = null; setSourceDrmSession(null);
} }
} }

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -62,7 +62,7 @@ public final class CryptoInfo {
private final PatternHolderV24 patternHolder; private final PatternHolderV24 patternHolder;
public CryptoInfo() { public CryptoInfo() {
frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null; frameworkCryptoInfo = new android.media.MediaCodec.CryptoInfo();
patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null; patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null;
} }
@ -79,34 +79,8 @@ public final class CryptoInfo {
this.mode = mode; this.mode = mode;
this.encryptedBlocks = encryptedBlocks; this.encryptedBlocks = encryptedBlocks;
this.clearBlocks = clearBlocks; this.clearBlocks = clearBlocks;
if (Util.SDK_INT >= 16) { // Update frameworkCryptoInfo fields directly because CryptoInfo.set performs an unnecessary
updateFrameworkCryptoInfoV16(); // object allocation on Android N.
}
}
/**
* Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
* <p>
* Successive calls to this method on a single {@link CryptoInfo} will return the same instance.
* Changes to the {@link CryptoInfo} will be reflected in the returned object. The return object
* should not be modified directly.
*
* @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
*/
@TargetApi(16)
public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {
return frameworkCryptoInfo;
}
@TargetApi(16)
private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() {
return new android.media.MediaCodec.CryptoInfo();
}
@TargetApi(16)
private void updateFrameworkCryptoInfoV16() {
// Update fields directly because the framework's CryptoInfo.set performs an unnecessary object
// allocation on Android N.
frameworkCryptoInfo.numSubSamples = numSubSamples; frameworkCryptoInfo.numSubSamples = numSubSamples;
frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData; frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData;
frameworkCryptoInfo.numBytesOfEncryptedData = numBytesOfEncryptedData; frameworkCryptoInfo.numBytesOfEncryptedData = numBytesOfEncryptedData;
@ -118,6 +92,25 @@ public final class CryptoInfo {
} }
} }
/**
* Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
*
* <p>Successive calls to this method on a single {@link CryptoInfo} will return the same
* instance. Changes to the {@link CryptoInfo} will be reflected in the returned object. The
* return object should not be modified directly.
*
* @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
*/
public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfo() {
return frameworkCryptoInfo;
}
/** @deprecated Use {@link #getFrameworkCryptoInfo()}. */
@Deprecated
public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {
return getFrameworkCryptoInfo();
}
@TargetApi(24) @TargetApi(24)
private static final class PatternHolderV24 { private static final class PatternHolderV24 {

View File

@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi;
import android.media.MediaDrm; import android.media.MediaDrm;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -27,7 +26,6 @@ import java.util.Map;
/** /**
* A DRM session. * A DRM session.
*/ */
@TargetApi(16)
public interface DrmSession<T extends ExoMediaCrypto> { public interface DrmSession<T extends ExoMediaCrypto> {
/** /**

View File

@ -15,14 +15,12 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi;
import android.os.Looper; import android.os.Looper;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
/** /**
* Manages a DRM session. * Manages a DRM session.
*/ */
@TargetApi(16)
public interface DrmSessionManager<T extends ExoMediaCrypto> { public interface DrmSessionManager<T extends ExoMediaCrypto> {
/** /**

View File

@ -15,14 +15,5 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
/** /** An opaque {@link android.media.MediaCrypto} equivalent. */
* An opaque {@link android.media.MediaCrypto} equivalent. public interface ExoMediaCrypto {}
*/
public interface ExoMediaCrypto {
/**
* @see android.media.MediaCrypto#requiresSecureDecoderComponent(String)
*/
boolean requiresSecureDecoderComponent(String mimeType);
}

View File

@ -265,11 +265,9 @@ public interface ExoMediaDrm<T extends ExoMediaCrypto> {
/** /**
* @see android.media.MediaCrypto#MediaCrypto(UUID, byte[]) * @see android.media.MediaCrypto#MediaCrypto(UUID, byte[])
* * @param sessionId The DRM session ID.
* @param initData Opaque initialization data specific to the crypto scheme.
* @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data. * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.
* @throws MediaCryptoException If the instance can't be created. * @throws MediaCryptoException If the instance can't be created.
*/ */
T createMediaCrypto(byte[] initData) throws MediaCryptoException; T createMediaCrypto(byte[] sessionId) throws MediaCryptoException;
} }

View File

@ -15,50 +15,35 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi;
import android.media.MediaCrypto; import android.media.MediaCrypto;
import com.google.android.exoplayer2.util.Assertions; import java.util.UUID;
/** /**
* An {@link ExoMediaCrypto} implementation that wraps the framework {@link MediaCrypto}. * An {@link ExoMediaCrypto} implementation that contains the necessary information to build or
* update a framework {@link MediaCrypto}.
*/ */
@TargetApi(16)
public final class FrameworkMediaCrypto implements ExoMediaCrypto { public final class FrameworkMediaCrypto implements ExoMediaCrypto {
private final MediaCrypto mediaCrypto; /** The DRM scheme UUID. */
private final boolean forceAllowInsecureDecoderComponents; public final UUID uuid;
/** The DRM session id. */
public final byte[] sessionId;
/**
* Whether to allow use of insecure decoder components even if the underlying platform says
* otherwise.
*/
public final boolean forceAllowInsecureDecoderComponents;
/** /**
* @param mediaCrypto The {@link MediaCrypto} to wrap. * @param uuid The DRM scheme UUID.
* @param sessionId The DRM session id.
* @param forceAllowInsecureDecoderComponents Whether to allow use of insecure decoder components
* even if the underlying platform says otherwise.
*/ */
public FrameworkMediaCrypto(MediaCrypto mediaCrypto) { public FrameworkMediaCrypto(
this(mediaCrypto, false); UUID uuid, byte[] sessionId, boolean forceAllowInsecureDecoderComponents) {
} this.uuid = uuid;
this.sessionId = sessionId;
/**
* @param mediaCrypto The {@link MediaCrypto} to wrap.
* @param forceAllowInsecureDecoderComponents Whether to force
* {@link #requiresSecureDecoderComponent(String)} to return {@code false}, rather than
* {@link MediaCrypto#requiresSecureDecoderComponent(String)} of the wrapped
* {@link MediaCrypto}.
*/
public FrameworkMediaCrypto(MediaCrypto mediaCrypto,
boolean forceAllowInsecureDecoderComponents) {
this.mediaCrypto = Assertions.checkNotNull(mediaCrypto);
this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents; this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;
} }
/**
* Returns the wrapped {@link MediaCrypto}.
*/
public MediaCrypto getWrappedMediaCrypto() {
return mediaCrypto;
}
@Override
public boolean requiresSecureDecoderComponent(String mimeType) {
return !forceAllowInsecureDecoderComponents
&& mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
} }

View File

@ -18,7 +18,6 @@ package com.google.android.exoplayer2.drm;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.DeniedByServerException; import android.media.DeniedByServerException;
import android.media.MediaCrypto;
import android.media.MediaCryptoException; import android.media.MediaCryptoException;
import android.media.MediaDrm; import android.media.MediaDrm;
import android.media.MediaDrmException; import android.media.MediaDrmException;
@ -210,7 +209,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21 boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
&& C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel")); && C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
return new FrameworkMediaCrypto( return new FrameworkMediaCrypto(
new MediaCrypto(adjustUuid(uuid), initData), forceAllowInsecureDecoderComponents); adjustUuid(uuid), initData, forceAllowInsecureDecoderComponents);
} }
private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) { private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) {

View File

@ -64,6 +64,9 @@ import com.google.android.exoplayer2.util.Util;
this.flags = flags; this.flags = flags;
this.durationUs = durationUs; this.durationUs = durationUs;
sampleCount = offsets.length; sampleCount = offsets.length;
if (flags.length > 0) {
flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE;
}
} }
/** /**

View File

@ -50,7 +50,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
FLAG_IGNORE_H264_STREAM, FLAG_IGNORE_H264_STREAM,
FLAG_DETECT_ACCESS_UNITS, FLAG_DETECT_ACCESS_UNITS,
FLAG_IGNORE_SPLICE_INFO_STREAM, FLAG_IGNORE_SPLICE_INFO_STREAM,
FLAG_OVERRIDE_CAPTION_DESCRIPTORS FLAG_OVERRIDE_CAPTION_DESCRIPTORS,
FLAG_IGNORE_HDMV_DTS_STREAM
}) })
public @interface Flags {} public @interface Flags {}
@ -86,6 +87,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
* closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors. * closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors.
*/ */
public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5; public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5;
/**
* Prevents the creation of {@link DtsReader} instances when receiving {@link
* TsExtractor#TS_STREAM_TYPE_HDMV_DTS} as stream type. Enabling this flag prevents a stream type
* collision between HDMV DTS audio and SCTE-35 subtitles.
*/
public static final int FLAG_IGNORE_HDMV_DTS_STREAM = 1 << 6;
private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86; private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86;
@ -142,8 +149,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
case TsExtractor.TS_STREAM_TYPE_AC3: case TsExtractor.TS_STREAM_TYPE_AC3:
case TsExtractor.TS_STREAM_TYPE_E_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3:
return new PesReader(new Ac3Reader(esInfo.language)); return new PesReader(new Ac3Reader(esInfo.language));
case TsExtractor.TS_STREAM_TYPE_DTS:
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
if (isSet(FLAG_IGNORE_HDMV_DTS_STREAM)) {
return null;
}
// Fall through.
case TsExtractor.TS_STREAM_TYPE_DTS:
return new PesReader(new DtsReader(esInfo.language)); return new PesReader(new DtsReader(esInfo.language));
case TsExtractor.TS_STREAM_TYPE_H262: case TsExtractor.TS_STREAM_TYPE_H262:
return new PesReader(new H262Reader(buildUserDataReader(esInfo))); return new PesReader(new H262Reader(buildUserDataReader(esInfo)));

View File

@ -100,7 +100,7 @@ public interface TsPayloadReader {
public final byte[] initializationData; public final byte[] initializationData;
/** /**
* @param language The ISO 639-2 three character language. * @param language The ISO 639-2 three-letter language code.
* @param type The subtitling type. * @param type The subtitling type.
* @param initializationData The composition and ancillary page ids. * @param initializationData The composition and ancillary page ids.
*/ */

View File

@ -31,7 +31,6 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** Information about a {@link MediaCodec} for a given mime type. */ /** Information about a {@link MediaCodec} for a given mime type. */
@TargetApi(16)
@SuppressWarnings("InlinedApi") @SuppressWarnings("InlinedApi")
public final class MediaCodecInfo { public final class MediaCodecInfo {

View File

@ -20,6 +20,7 @@ import android.media.MediaCodec;
import android.media.MediaCodec.CodecException; import android.media.MediaCodec.CodecException;
import android.media.MediaCodec.CryptoException; import android.media.MediaCodec.CryptoException;
import android.media.MediaCrypto; import android.media.MediaCrypto;
import android.media.MediaCryptoException;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Bundle; import android.os.Bundle;
import android.os.Looper; import android.os.Looper;
@ -57,7 +58,6 @@ import java.util.List;
/** /**
* An abstract renderer that uses {@link MediaCodec} to decode samples for rendering. * An abstract renderer that uses {@link MediaCodec} to decode samples for rendering.
*/ */
@TargetApi(16)
public abstract class MediaCodecRenderer extends BaseRenderer { public abstract class MediaCodecRenderer extends BaseRenderer {
/** /**
@ -239,14 +239,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({DRAIN_ACTION_NONE, DRAIN_ACTION_FLUSH, DRAIN_ACTION_REINITIALIZE}) @IntDef({
DRAIN_ACTION_NONE,
DRAIN_ACTION_FLUSH,
DRAIN_ACTION_UPDATE_DRM_SESSION,
DRAIN_ACTION_REINITIALIZE
})
private @interface DrainAction {} private @interface DrainAction {}
/** No special action should be taken. */ /** No special action should be taken. */
private static final int DRAIN_ACTION_NONE = 0; private static final int DRAIN_ACTION_NONE = 0;
/** The codec should be flushed. */ /** The codec should be flushed. */
private static final int DRAIN_ACTION_FLUSH = 1; private static final int DRAIN_ACTION_FLUSH = 1;
/** The codec should be re-initialized. */ /** The codec should be flushed and updated to use the pending DRM session. */
private static final int DRAIN_ACTION_REINITIALIZE = 2; private static final int DRAIN_ACTION_UPDATE_DRM_SESSION = 2;
/** The codec should be reinitialized. */
private static final int DRAIN_ACTION_REINITIALIZE = 3;
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@ -287,13 +294,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private final DecoderInputBuffer flagsOnlyBuffer; private final DecoderInputBuffer flagsOnlyBuffer;
private final FormatHolder formatHolder; private final FormatHolder formatHolder;
private final TimedValueQueue<Format> formatQueue; private final TimedValueQueue<Format> formatQueue;
private final List<Long> decodeOnlyPresentationTimestamps; private final ArrayList<Long> decodeOnlyPresentationTimestamps;
private final MediaCodec.BufferInfo outputBufferInfo; private final MediaCodec.BufferInfo outputBufferInfo;
@Nullable private Format inputFormat; @Nullable private Format inputFormat;
private Format outputFormat; private Format outputFormat;
private DrmSession<FrameworkMediaCrypto> drmSession; @Nullable private DrmSession<FrameworkMediaCrypto> codecDrmSession;
private DrmSession<FrameworkMediaCrypto> pendingDrmSession; @Nullable private DrmSession<FrameworkMediaCrypto> sourceDrmSession;
@Nullable private MediaCrypto mediaCrypto;
private boolean mediaCryptoRequiresSecureDecoder;
private long renderTimeLimitMs; private long renderTimeLimitMs;
private float rendererOperatingRate; private float rendererOperatingRate;
@Nullable private MediaCodec codec; @Nullable private MediaCodec codec;
@ -356,7 +365,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
boolean playClearSamplesWithoutKeys, boolean playClearSamplesWithoutKeys,
float assumedMinimumCodecOperatingRate) { float assumedMinimumCodecOperatingRate) {
super(trackType); super(trackType);
Assertions.checkState(Util.SDK_INT >= 16);
this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector); this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector);
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
@ -457,29 +465,36 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return; return;
} }
drmSession = pendingDrmSession; setCodecDrmSession(sourceDrmSession);
String mimeType = inputFormat.sampleMimeType; String mimeType = inputFormat.sampleMimeType;
MediaCrypto wrappedMediaCrypto = null; if (codecDrmSession != null) {
boolean drmSessionRequiresSecureDecoder = false;
if (drmSession != null) {
FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto();
if (mediaCrypto == null) { if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError(); FrameworkMediaCrypto sessionMediaCrypto = codecDrmSession.getMediaCrypto();
if (sessionMediaCrypto == null) {
DrmSessionException drmError = codecDrmSession.getError();
if (drmError != null) { if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a new // Continue for now. We may be able to avoid failure if the session recovers, or if a
// input format causes the session to be replaced before it's used. // new input format causes the session to be replaced before it's used.
} else { } else {
// The drm session isn't open yet. // The drm session isn't open yet.
return; return;
} }
} else { } else {
wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto(); try {
drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId);
} catch (MediaCryptoException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
mediaCryptoRequiresSecureDecoder =
!sessionMediaCrypto.forceAllowInsecureDecoderComponents
&& mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
} }
if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) { if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
@DrmSession.State int drmSessionState = drmSession.getState(); @DrmSession.State int drmSessionState = codecDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) { if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
} else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) { } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
// Wait for keys. // Wait for keys.
return; return;
@ -488,7 +503,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
try { try {
maybeInitCodecWithFallback(wrappedMediaCrypto, drmSessionRequiresSecureDecoder); maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder);
} catch (DecoderInitializationException e) { } catch (DecoderInitializationException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex()); throw ExoPlaybackException.createForRenderer(e, getIndex());
} }
@ -537,7 +552,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
flushOrReinitCodec(); flushOrReinitializeCodec();
formatQueue.clear(); formatQueue.clear();
} }
@ -552,7 +567,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Override @Override
protected void onDisabled() { protected void onDisabled() {
inputFormat = null; inputFormat = null;
if (drmSession != null || pendingDrmSession != null) { if (sourceDrmSession != null || codecDrmSession != null) {
// TODO: Do something better with this case. // TODO: Do something better with this case.
onReset(); onReset();
} else { } else {
@ -565,26 +580,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
try { try {
releaseCodec(); releaseCodec();
} finally { } finally {
try { setSourceDrmSession(null);
if (drmSession != null) {
drmSessionManager.releaseSession(drmSession);
}
} finally {
try {
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} finally {
drmSession = null;
pendingDrmSession = null;
}
}
} }
} }
protected void releaseCodec() { protected void releaseCodec() {
availableCodecInfos = null; availableCodecInfos = null;
if (codec != null) {
codecInfo = null; codecInfo = null;
codecFormat = null; codecFormat = null;
resetInputBuffer(); resetInputBuffer();
@ -593,22 +594,25 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
waitingForKeys = false; waitingForKeys = false;
codecHotswapDeadlineMs = C.TIME_UNSET; codecHotswapDeadlineMs = C.TIME_UNSET;
decodeOnlyPresentationTimestamps.clear(); decodeOnlyPresentationTimestamps.clear();
try {
if (codec != null) {
decoderCounters.decoderReleaseCount++; decoderCounters.decoderReleaseCount++;
try { try {
codec.stop(); codec.stop();
} finally { } finally {
try {
codec.release(); codec.release();
}
}
} finally { } finally {
codec = null; codec = null;
if (drmSession != null && pendingDrmSession != drmSession) {
try { try {
drmSessionManager.releaseSession(drmSession); if (mediaCrypto != null) {
mediaCrypto.release();
}
} finally { } finally {
drmSession = null; mediaCrypto = null;
} mediaCryptoRequiresSecureDecoder = false;
} setCodecDrmSession(null);
}
} }
} }
} }
@ -680,12 +684,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* <p>The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link * <p>The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link
* #maybeInitCodec()} if the codec needs to be re-instantiated. * #maybeInitCodec()} if the codec needs to be re-instantiated.
* *
* @return Whether the codec was released and reinitialized, rather than being flushed.
* @throws ExoPlaybackException If an error occurs re-instantiating the codec. * @throws ExoPlaybackException If an error occurs re-instantiating the codec.
*/ */
protected final void flushOrReinitCodec() throws ExoPlaybackException { protected final boolean flushOrReinitializeCodec() throws ExoPlaybackException {
if (flushOrReleaseCodec()) { boolean released = flushOrReleaseCodec();
if (released) {
maybeInitCodec(); maybeInitCodec();
} }
return released;
} }
/** /**
@ -729,18 +736,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
private void maybeInitCodecWithFallback( private void maybeInitCodecWithFallback(
MediaCrypto crypto, boolean drmSessionRequiresSecureDecoder) MediaCrypto crypto, boolean mediaCryptoRequiresSecureDecoder)
throws DecoderInitializationException { throws DecoderInitializationException {
if (availableCodecInfos == null) { if (availableCodecInfos == null) {
try { try {
availableCodecInfos = availableCodecInfos =
new ArrayDeque<>(getAvailableCodecInfos(drmSessionRequiresSecureDecoder)); new ArrayDeque<>(getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder));
preferredDecoderInitializationException = null; preferredDecoderInitializationException = null;
} catch (DecoderQueryException e) { } catch (DecoderQueryException e) {
throw new DecoderInitializationException( throw new DecoderInitializationException(
inputFormat, inputFormat,
e, e,
drmSessionRequiresSecureDecoder, mediaCryptoRequiresSecureDecoder,
DecoderInitializationException.DECODER_QUERY_ERROR); DecoderInitializationException.DECODER_QUERY_ERROR);
} }
} }
@ -749,7 +756,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
throw new DecoderInitializationException( throw new DecoderInitializationException(
inputFormat, inputFormat,
/* cause= */ null, /* cause= */ null,
drmSessionRequiresSecureDecoder, mediaCryptoRequiresSecureDecoder,
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR); DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);
} }
@ -768,7 +775,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
availableCodecInfos.removeFirst(); availableCodecInfos.removeFirst();
DecoderInitializationException exception = DecoderInitializationException exception =
new DecoderInitializationException( new DecoderInitializationException(
inputFormat, e, drmSessionRequiresSecureDecoder, codecInfo.name); inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo.name);
if (preferredDecoderInitializationException == null) { if (preferredDecoderInitializationException == null) {
preferredDecoderInitializationException = exception; preferredDecoderInitializationException = exception;
} else { } else {
@ -784,11 +791,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
availableCodecInfos = null; availableCodecInfos = null;
} }
private List<MediaCodecInfo> getAvailableCodecInfos(boolean drmSessionRequiresSecureDecoder) private List<MediaCodecInfo> getAvailableCodecInfos(boolean mediaCryptoRequiresSecureDecoder)
throws DecoderQueryException { throws DecoderQueryException {
List<MediaCodecInfo> codecInfos = List<MediaCodecInfo> codecInfos =
getDecoderInfos(mediaCodecSelector, inputFormat, drmSessionRequiresSecureDecoder); getDecoderInfos(mediaCodecSelector, inputFormat, mediaCryptoRequiresSecureDecoder);
if (codecInfos.isEmpty() && drmSessionRequiresSecureDecoder) { if (codecInfos.isEmpty() && mediaCryptoRequiresSecureDecoder) {
// The drm session indicates that a secure decoder is required, but the device does not // The drm session indicates that a secure decoder is required, but the device does not
// have one. Assuming that supportsFormat indicated support for the media being played, we // have one. Assuming that supportsFormat indicated support for the media being played, we
// know that it does not require a secure output path. Most CDM implementations allow // know that it does not require a secure output path. Most CDM implementations allow
@ -928,6 +935,24 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputBuffer = null; outputBuffer = null;
} }
private void setSourceDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) {
DrmSession<FrameworkMediaCrypto> previous = sourceDrmSession;
sourceDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void setCodecDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) {
DrmSession<FrameworkMediaCrypto> previous = codecDrmSession;
codecDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void releaseDrmSessionIfUnused(@Nullable DrmSession<FrameworkMediaCrypto> session) {
if (session != null && session != sourceDrmSession && session != codecDrmSession) {
drmSessionManager.releaseSession(session);
}
}
/** /**
* @return Whether it may be possible to feed more input data. * @return Whether it may be possible to feed more input data.
* @throws ExoPlaybackException If an error occurs feeding the input buffer. * @throws ExoPlaybackException If an error occurs feeding the input buffer.
@ -1082,12 +1107,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { if (codecDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false; return false;
} }
@DrmSession.State int drmSessionState = drmSession.getState(); @DrmSession.State int drmSessionState = codecDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) { if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
} }
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
} }
@ -1126,13 +1151,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
throw ExoPlaybackException.createForRenderer( throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
} }
pendingDrmSession = DrmSession<FrameworkMediaCrypto> session =
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
if (pendingDrmSession == drmSession) { if (session == sourceDrmSession || session == codecDrmSession) {
drmSessionManager.releaseSession(pendingDrmSession); // We already had this session. The manager must be reference counting, so release it once
// to get the count attributed to this renderer back down to 1.
drmSessionManager.releaseSession(session);
} }
setSourceDrmSession(session);
} else { } else {
pendingDrmSession = null; setSourceDrmSession(null);
} }
} }
@ -1143,17 +1171,30 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
// We have an existing codec that we may need to reconfigure or re-initialize. If the existing // We have an existing codec that we may need to reconfigure or re-initialize. If the existing
// codec instance is being kept then its operating rate may need to be updated. // codec instance is being kept then its operating rate may need to be updated.
if (pendingDrmSession != drmSession) {
if ((sourceDrmSession == null && codecDrmSession != null)
|| (sourceDrmSession != null && codecDrmSession == null)
|| (sourceDrmSession != null && !codecInfo.secure)
|| (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) {
// We might need to switch between the clear and protected output paths, or we're using DRM
// prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM
// session.
drainAndReinitializeCodec(); drainAndReinitializeCodec();
} else { return;
}
switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) { switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {
case KEEP_CODEC_RESULT_NO: case KEEP_CODEC_RESULT_NO:
drainAndReinitializeCodec(); drainAndReinitializeCodec();
break; break;
case KEEP_CODEC_RESULT_YES_WITH_FLUSH: case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
drainAndFlushCodec();
codecFormat = newFormat; codecFormat = newFormat;
updateCodecOperatingRate(); updateCodecOperatingRate();
if (sourceDrmSession != codecDrmSession) {
drainAndUpdateCodecDrmSession();
} else {
drainAndFlushCodec();
}
break; break;
case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION: case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION:
if (codecNeedsReconfigureWorkaround) { if (codecNeedsReconfigureWorkaround) {
@ -1168,17 +1209,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&& newFormat.height == codecFormat.height); && newFormat.height == codecFormat.height);
codecFormat = newFormat; codecFormat = newFormat;
updateCodecOperatingRate(); updateCodecOperatingRate();
if (sourceDrmSession != codecDrmSession) {
drainAndUpdateCodecDrmSession();
}
} }
break; break;
case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION: case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:
codecFormat = newFormat; codecFormat = newFormat;
updateCodecOperatingRate(); updateCodecOperatingRate();
if (sourceDrmSession != codecDrmSession) {
drainAndUpdateCodecDrmSession();
}
break; break;
default: default:
throw new IllegalStateException(); // Never happens. throw new IllegalStateException(); // Never happens.
} }
} }
}
/** /**
* Called when the output format of the {@link MediaCodec} changes. * Called when the output format of the {@link MediaCodec} changes.
@ -1311,6 +1357,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
} }
/**
* Starts draining the codec to update its DRM session. The update may occur immediately if no
* buffers have been queued to the codec.
*
* @throws ExoPlaybackException If an error occurs updating the codec's DRM session.
*/
private void drainAndUpdateCodecDrmSession() throws ExoPlaybackException {
if (Util.SDK_INT < 23) {
// The codec needs to be re-initialized to switch to the source DRM session.
drainAndReinitializeCodec();
return;
}
if (codecReceivedBuffers) {
codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM;
codecDrainAction = DRAIN_ACTION_UPDATE_DRM_SESSION;
} else {
// Nothing has been queued to the decoder, so we can do the update immediately.
updateDrmSessionOrReinitializeCodecV23();
}
}
/** /**
* Starts draining the codec for re-initialization. Re-initialization may occur immediately if no * Starts draining the codec for re-initialization. Re-initialization may occur immediately if no
* buffers have been queued to the codec. * buffers have been queued to the codec.
@ -1323,8 +1390,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecDrainAction = DRAIN_ACTION_REINITIALIZE; codecDrainAction = DRAIN_ACTION_REINITIALIZE;
} else { } else {
// Nothing has been queued to the decoder, so we can re-initialize immediately. // Nothing has been queued to the decoder, so we can re-initialize immediately.
releaseCodec(); reinitializeCodec();
maybeInitCodec();
} }
} }
@ -1528,11 +1594,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private void processEndOfStream() throws ExoPlaybackException { private void processEndOfStream() throws ExoPlaybackException {
switch (codecDrainAction) { switch (codecDrainAction) {
case DRAIN_ACTION_REINITIALIZE: case DRAIN_ACTION_REINITIALIZE:
releaseCodec(); reinitializeCodec();
maybeInitCodec(); break;
case DRAIN_ACTION_UPDATE_DRM_SESSION:
updateDrmSessionOrReinitializeCodecV23();
break; break;
case DRAIN_ACTION_FLUSH: case DRAIN_ACTION_FLUSH:
flushOrReinitCodec(); flushOrReinitializeCodec();
break; break;
case DRAIN_ACTION_NONE: case DRAIN_ACTION_NONE:
default: default:
@ -1542,6 +1610,41 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
} }
private void reinitializeCodec() throws ExoPlaybackException {
releaseCodec();
maybeInitCodec();
}
@TargetApi(23)
private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackException {
FrameworkMediaCrypto sessionMediaCrypto = sourceDrmSession.getMediaCrypto();
if (sessionMediaCrypto == null) {
// We'd only expect this to happen if the CDM from which the pending session is obtained needs
// provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme
// to another, where the new CDM hasn't been used before and needs provisioning). It would be
// possible to handle this case more efficiently (i.e. with a new renderer state that waits
// for provisioning to finish and then calls mediaCrypto.setMediaDrmSession), but the extra
// complexity is not warranted given how unlikely the case is to occur.
reinitializeCodec();
return;
}
if (flushOrReinitializeCodec()) {
// The codec was reinitialized. The new codec will be using the new DRM session, so there's
// nothing more to do.
return;
}
try {
mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId);
} catch (MediaCryptoException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
setCodecDrmSession(sourceDrmSession);
codecDrainState = DRAIN_STATE_NONE;
codecDrainAction = DRAIN_ACTION_NONE;
}
private boolean shouldSkipOutputBuffer(long presentationTimeUs) { private boolean shouldSkipOutputBuffer(long presentationTimeUs) {
// We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would
// box presentationTimeUs, creating a Long object that would need to be garbage collected. // box presentationTimeUs, creating a Long object that would need to be garbage collected.
@ -1557,7 +1660,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private static MediaCodec.CryptoInfo getFrameworkCryptoInfo( private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(
DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) { DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) {
MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16(); MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfo();
if (adaptiveReconfigurationBytes == 0) { if (adaptiveReconfigurationBytes == 0) {
return cryptoInfo; return cryptoInfo;
} }

View File

@ -39,7 +39,6 @@ import java.util.regex.Pattern;
/** /**
* A utility class for querying the available codecs. * A utility class for querying the available codecs.
*/ */
@TargetApi(16)
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
public final class MediaCodecUtil { public final class MediaCodecUtil {
@ -59,8 +58,6 @@ public final class MediaCodecUtil {
private static final String TAG = "MediaCodecUtil"; private static final String TAG = "MediaCodecUtil";
private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$"); private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$");
private static final RawAudioCodecComparator RAW_AUDIO_CODEC_COMPARATOR =
new RawAudioCodecComparator();
private static final HashMap<CodecKey, List<MediaCodecInfo>> decoderInfosCache = new HashMap<>(); private static final HashMap<CodecKey, List<MediaCodecInfo>> decoderInfosCache = new HashMap<>();
@ -312,30 +309,6 @@ public final class MediaCodecUtil {
return false; return false;
} }
// Work around https://github.com/google/ExoPlayer/issues/398.
if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) {
return false;
}
// Work around https://github.com/google/ExoPlayer/issues/4519.
if ("OMX.SEC.mp3.dec".equals(name)
&& (Util.MODEL.startsWith("GT-I9152")
|| Util.MODEL.startsWith("GT-I9515")
|| Util.MODEL.startsWith("GT-P5220")
|| Util.MODEL.startsWith("GT-S7580")
|| Util.MODEL.startsWith("SM-G350")
|| Util.MODEL.startsWith("SM-G386")
|| Util.MODEL.startsWith("SM-T231")
|| Util.MODEL.startsWith("SM-T530"))) {
return false;
}
if ("OMX.brcm.audio.mp3.decoder".equals(name)
&& (Util.MODEL.startsWith("GT-I9152")
|| Util.MODEL.startsWith("GT-S7580")
|| Util.MODEL.startsWith("SM-G350"))) {
return false;
}
// Work around https://github.com/google/ExoPlayer/issues/1528 and // Work around https://github.com/google/ExoPlayer/issues/1528 and
// https://github.com/google/ExoPlayer/issues/3171. // https://github.com/google/ExoPlayer/issues/3171.
if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name)
@ -422,7 +395,18 @@ public final class MediaCodecUtil {
*/ */
private static void applyWorkarounds(String mimeType, List<MediaCodecInfo> decoderInfos) { private static void applyWorkarounds(String mimeType, List<MediaCodecInfo> decoderInfos) {
if (MimeTypes.AUDIO_RAW.equals(mimeType)) { if (MimeTypes.AUDIO_RAW.equals(mimeType)) {
Collections.sort(decoderInfos, RAW_AUDIO_CODEC_COMPARATOR); Collections.sort(decoderInfos, new RawAudioCodecComparator());
} else if (Util.SDK_INT < 21 && decoderInfos.size() > 1) {
String firstCodecName = decoderInfos.get(0).name;
if ("OMX.SEC.mp3.dec".equals(firstCodecName)
|| "OMX.SEC.MP3.Decoder".equals(firstCodecName)
|| "OMX.brcm.audio.mp3.decoder".equals(firstCodecName)) {
// Prefer OMX.google codecs over OMX.SEC.mp3.dec, OMX.SEC.MP3.Decoder and
// OMX.brcm.audio.mp3.decoder on older devices. See:
// https://github.com/google/ExoPlayer/issues/398 and
// https://github.com/google/ExoPlayer/issues/4519.
Collections.sort(decoderInfos, new PreferOmxGoogleCodecComparator());
}
} }
} }
@ -461,9 +445,10 @@ public final class MediaCodecUtil {
Log.w(TAG, "Unknown HEVC profile string: " + profileString); Log.w(TAG, "Unknown HEVC profile string: " + profileString);
return null; return null;
} }
Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(parts[3]); String levelString = parts[3];
Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(levelString);
if (level == null) { if (level == null) {
Log.w(TAG, "Unknown HEVC level string: " + matcher.group(1)); Log.w(TAG, "Unknown HEVC level string: " + levelString);
return null; return null;
} }
return new Pair<>(profile, level); return new Pair<>(profile, level);
@ -728,6 +713,18 @@ public final class MediaCodecUtil {
} }
} }
/** Comparator for preferring OMX.google media codecs. */
private static final class PreferOmxGoogleCodecComparator implements Comparator<MediaCodecInfo> {
@Override
public int compare(MediaCodecInfo a, MediaCodecInfo b) {
return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b);
}
private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) {
return mediaCodecInfo.name.startsWith("OMX.google") ? -1 : 0;
}
}
static { static {
AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray();
AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline); AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline);

View File

@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.mediacodec; package com.google.android.exoplayer2.mediacodec;
import android.annotation.TargetApi;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
@ -24,7 +23,6 @@ import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
/** Helper class for configuring {@link MediaFormat} instances. */ /** Helper class for configuring {@link MediaFormat} instances. */
@TargetApi(16)
public final class MediaFormatUtil { public final class MediaFormatUtil {
private MediaFormatUtil() {} private MediaFormatUtil() {}

View File

@ -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();
}
}
}

View File

@ -156,7 +156,7 @@ public final class DownloadAction {
ArrayList<StreamKey> mutableKeys = new ArrayList<>(keys); ArrayList<StreamKey> mutableKeys = new ArrayList<>(keys);
Collections.sort(mutableKeys); Collections.sort(mutableKeys);
this.keys = Collections.unmodifiableList(mutableKeys); this.keys = Collections.unmodifiableList(mutableKeys);
this.data = data != null ? data : Util.EMPTY_BYTE_ARRAY; this.data = data != null ? Arrays.copyOf(data, data.length) : Util.EMPTY_BYTE_ARRAY;
} }
} }

View File

@ -17,8 +17,11 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.os.Message;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Pair;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
@ -27,6 +30,8 @@ import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
@ -36,11 +41,16 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Paramet
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -58,19 +68,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* <p>A typical usage of DownloadHelper follows these steps: * <p>A typical usage of DownloadHelper follows these steps:
* *
* <ol> * <ol>
* <li>Construct the download helper with information about the {@link RenderersFactory renderers} * <li>Build the helper using one of the {@code forXXX} methods.
* and {@link DefaultTrackSelector.Parameters parameters} for track selection.
* <li>Prepare the helper using {@link #prepare(Callback)} and wait for the callback. * <li>Prepare the helper using {@link #prepare(Callback)} and wait for the callback.
* <li>Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link * <li>Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link
* #getTrackSelections(int, int)}, and make adjustments using {@link * #getTrackSelections(int, int)}, and make adjustments using {@link
* #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link * #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link
* #addTrackSelection(int, Parameters)}. * #addTrackSelection(int, Parameters)}.
* <li>Create download actions for the selected track using {@link #getDownloadAction(byte[])}. * <li>Create a download action for the selected track using {@link #getDownloadAction(byte[])}.
* <li>Release the helper using {@link #release()}.
* </ol> * </ol>
*
* @param <T> The manifest type.
*/ */
public abstract class DownloadHelper<T> { public final class DownloadHelper {
/** /**
* The default parameters used for track selection for downloading. This default selects the * The default parameters used for track selection for downloading. This default selects the
@ -87,7 +95,7 @@ public abstract class DownloadHelper<T> {
* *
* @param helper The reporting {@link DownloadHelper}. * @param helper The reporting {@link DownloadHelper}.
*/ */
void onPrepared(DownloadHelper<?> helper); void onPrepared(DownloadHelper helper);
/** /**
* Called when preparation fails. * Called when preparation fails.
@ -95,18 +103,222 @@ public abstract class DownloadHelper<T> {
* @param helper The reporting {@link DownloadHelper}. * @param helper The reporting {@link DownloadHelper}.
* @param e The error. * @param e The error.
*/ */
void onPrepareError(DownloadHelper<?> helper, IOException e); void onPrepareError(DownloadHelper helper, IOException e);
}
@Nullable private static final Constructor<?> DASH_FACTORY_CONSTRUCTOR;
@Nullable private static final Constructor<?> HLS_FACTORY_CONSTRUCTOR;
@Nullable private static final Constructor<?> SS_FACTORY_CONSTRUCTOR;
@Nullable private static final Method DASH_FACTORY_CREATE_METHOD;
@Nullable private static final Method HLS_FACTORY_CREATE_METHOD;
@Nullable private static final Method SS_FACTORY_CREATE_METHOD;
static {
Pair<@NullableType Constructor<?>, @NullableType Method> dashFactoryMethods =
getMediaSourceFactoryMethods(
"com.google.android.exoplayer2.source.dash.DashMediaSource$Factory");
DASH_FACTORY_CONSTRUCTOR = dashFactoryMethods.first;
DASH_FACTORY_CREATE_METHOD = dashFactoryMethods.second;
Pair<@NullableType Constructor<?>, @NullableType Method> hlsFactoryMethods =
getMediaSourceFactoryMethods(
"com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory");
HLS_FACTORY_CONSTRUCTOR = hlsFactoryMethods.first;
HLS_FACTORY_CREATE_METHOD = hlsFactoryMethods.second;
Pair<@NullableType Constructor<?>, @NullableType Method> ssFactoryMethods =
getMediaSourceFactoryMethods(
"com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory");
SS_FACTORY_CONSTRUCTOR = ssFactoryMethods.first;
SS_FACTORY_CREATE_METHOD = ssFactoryMethods.second;
}
/**
* Creates a {@link DownloadHelper} for progressive streams.
*
* @param uri A stream {@link Uri}.
* @return A {@link DownloadHelper} for progressive streams.
*/
public static DownloadHelper forProgressive(Uri uri) {
return forProgressive(uri, /* cacheKey= */ null);
}
/**
* Creates a {@link DownloadHelper} for progressive streams.
*
* @param uri A stream {@link Uri}.
* @param cacheKey An optional cache key.
* @return A {@link DownloadHelper} for progressive streams.
*/
public static DownloadHelper forProgressive(Uri uri, @Nullable String cacheKey) {
return new DownloadHelper(
DownloadAction.TYPE_PROGRESSIVE,
uri,
cacheKey,
/* mediaSource= */ null,
DEFAULT_TRACK_SELECTOR_PARAMETERS,
/* rendererCapabilities= */ new RendererCapabilities[0]);
}
/**
* Creates a {@link DownloadHelper} for DASH streams.
*
* @param uri A manifest {@link Uri}.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
* selected.
* @return A {@link DownloadHelper} for DASH streams.
* @throws IllegalStateException If the DASH module is missing.
*/
public static DownloadHelper forDash(
Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {
return forDash(
uri,
dataSourceFactory,
renderersFactory,
/* drmSessionManager= */ null,
DEFAULT_TRACK_SELECTOR_PARAMETERS);
}
/**
* Creates a {@link DownloadHelper} for DASH streams.
*
* @param uri A manifest {@link Uri}.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
* selected.
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
* {@code renderersFactory}.
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
* downloading.
* @return A {@link DownloadHelper} for DASH streams.
* @throws IllegalStateException If the DASH module is missing.
*/
public static DownloadHelper forDash(
Uri uri,
DataSource.Factory dataSourceFactory,
RenderersFactory renderersFactory,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
DefaultTrackSelector.Parameters trackSelectorParameters) {
return new DownloadHelper(
DownloadAction.TYPE_DASH,
uri,
/* cacheKey= */ null,
createMediaSource(
uri, dataSourceFactory, DASH_FACTORY_CONSTRUCTOR, DASH_FACTORY_CREATE_METHOD),
trackSelectorParameters,
Util.getRendererCapabilities(renderersFactory, drmSessionManager));
}
/**
* Creates a {@link DownloadHelper} for HLS streams.
*
* @param uri A playlist {@link Uri}.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist.
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
* selected.
* @return A {@link DownloadHelper} for HLS streams.
* @throws IllegalStateException If the HLS module is missing.
*/
public static DownloadHelper forHls(
Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {
return forHls(
uri,
dataSourceFactory,
renderersFactory,
/* drmSessionManager= */ null,
DEFAULT_TRACK_SELECTOR_PARAMETERS);
}
/**
* Creates a {@link DownloadHelper} for HLS streams.
*
* @param uri A playlist {@link Uri}.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist.
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
* selected.
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
* {@code renderersFactory}.
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
* downloading.
* @return A {@link DownloadHelper} for HLS streams.
* @throws IllegalStateException If the HLS module is missing.
*/
public static DownloadHelper forHls(
Uri uri,
DataSource.Factory dataSourceFactory,
RenderersFactory renderersFactory,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
DefaultTrackSelector.Parameters trackSelectorParameters) {
return new DownloadHelper(
DownloadAction.TYPE_HLS,
uri,
/* cacheKey= */ null,
createMediaSource(
uri, dataSourceFactory, HLS_FACTORY_CONSTRUCTOR, HLS_FACTORY_CREATE_METHOD),
trackSelectorParameters,
Util.getRendererCapabilities(renderersFactory, drmSessionManager));
}
/**
* Creates a {@link DownloadHelper} for SmoothStreaming streams.
*
* @param uri A manifest {@link Uri}.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
* selected.
* @return A {@link DownloadHelper} for SmoothStreaming streams.
* @throws IllegalStateException If the SmoothStreaming module is missing.
*/
public static DownloadHelper forSmoothStreaming(
Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) {
return forSmoothStreaming(
uri,
dataSourceFactory,
renderersFactory,
/* drmSessionManager= */ null,
DEFAULT_TRACK_SELECTOR_PARAMETERS);
}
/**
* Creates a {@link DownloadHelper} for SmoothStreaming streams.
*
* @param uri A manifest {@link Uri}.
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
* selected.
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
* {@code renderersFactory}.
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
* downloading.
* @return A {@link DownloadHelper} for SmoothStreaming streams.
* @throws IllegalStateException If the SmoothStreaming module is missing.
*/
public static DownloadHelper forSmoothStreaming(
Uri uri,
DataSource.Factory dataSourceFactory,
RenderersFactory renderersFactory,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
DefaultTrackSelector.Parameters trackSelectorParameters) {
return new DownloadHelper(
DownloadAction.TYPE_SS,
uri,
/* cacheKey= */ null,
createMediaSource(uri, dataSourceFactory, SS_FACTORY_CONSTRUCTOR, SS_FACTORY_CREATE_METHOD),
trackSelectorParameters,
Util.getRendererCapabilities(renderersFactory, drmSessionManager));
} }
private final String downloadType; private final String downloadType;
private final Uri uri; private final Uri uri;
@Nullable private final String cacheKey; @Nullable private final String cacheKey;
@Nullable private final MediaSource mediaSource;
private final DefaultTrackSelector trackSelector; private final DefaultTrackSelector trackSelector;
private final RendererCapabilities[] rendererCapabilities; private final RendererCapabilities[] rendererCapabilities;
private final SparseIntArray scratchSet; private final SparseIntArray scratchSet;
private int currentTrackSelectionPeriodIndex; private boolean isPreparedWithMedia;
@Nullable private T manifest; private @MonotonicNonNull Callback callback;
private @MonotonicNonNull Handler callbackHandler;
private @MonotonicNonNull MediaPreparer mediaPreparer;
private TrackGroupArray @MonotonicNonNull [] trackGroupArrays; private TrackGroupArray @MonotonicNonNull [] trackGroupArrays;
private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos; private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos;
private List<TrackSelection> @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer; private List<TrackSelection> @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer;
@ -118,25 +330,26 @@ public abstract class DownloadHelper<T> {
* @param downloadType A download type. This value will be used as {@link DownloadAction#type}. * @param downloadType A download type. This value will be used as {@link DownloadAction#type}.
* @param uri A {@link Uri}. * @param uri A {@link Uri}.
* @param cacheKey An optional cache key. * @param cacheKey An optional cache key.
* @param mediaSource A {@link MediaSource} for which tracks are selected, or null if no track
* selection needs to be made.
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
* downloading. * downloading.
* @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks
* are selected. * are selected.
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
* {@code renderersFactory}.
*/ */
public DownloadHelper( public DownloadHelper(
String downloadType, String downloadType,
Uri uri, Uri uri,
@Nullable String cacheKey, @Nullable String cacheKey,
@Nullable MediaSource mediaSource,
DefaultTrackSelector.Parameters trackSelectorParameters, DefaultTrackSelector.Parameters trackSelectorParameters,
RenderersFactory renderersFactory, RendererCapabilities[] rendererCapabilities) {
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
this.downloadType = downloadType; this.downloadType = downloadType;
this.uri = uri; this.uri = uri;
this.cacheKey = cacheKey; this.cacheKey = cacheKey;
this.mediaSource = mediaSource;
this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory()); this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory());
this.rendererCapabilities = Util.getRendererCapabilities(renderersFactory, drmSessionManager); this.rendererCapabilities = rendererCapabilities;
this.scratchSet = new SparseIntArray(); this.scratchSet = new SparseIntArray();
trackSelector.setParameters(trackSelectorParameters); trackSelector.setParameters(trackSelectorParameters);
trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter());
@ -148,43 +361,49 @@ public abstract class DownloadHelper<T> {
* @param callback A callback to be notified when preparation completes or fails. The callback * @param callback A callback to be notified when preparation completes or fails. The callback
* will be invoked on the calling thread unless that thread does not have an associated {@link * will be invoked on the calling thread unless that thread does not have an associated {@link
* Looper}, in which case it will be called on the application's main thread. * Looper}, in which case it will be called on the application's main thread.
* @throws IllegalStateException If the download helper has already been prepared.
*/ */
public final void prepare(Callback callback) { public void prepare(Callback callback) {
Handler handler = Assertions.checkState(this.callback == null);
this.callback = callback;
callbackHandler =
new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper());
new Thread( if (mediaSource != null) {
() -> { mediaPreparer = new MediaPreparer(mediaSource, /* downloadHelper= */ this);
try { } else {
manifest = loadManifest(uri); callbackHandler.post(() -> callback.onPrepared(this));
trackGroupArrays = getTrackGroupArrays(manifest);
initializeTrackSelectionLists(trackGroupArrays.length, rendererCapabilities.length);
mappedTrackInfos = new MappedTrackInfo[trackGroupArrays.length];
for (int i = 0; i < trackGroupArrays.length; i++) {
TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i);
trackSelector.onSelectionActivated(trackSelectorResult.info);
mappedTrackInfos[i] =
Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
} }
handler.post(() -> callback.onPrepared(DownloadHelper.this));
} catch (final IOException e) {
handler.post(() -> callback.onPrepareError(DownloadHelper.this, e));
}
})
.start();
} }
/** Returns the manifest. Must not be called until after preparation completes. */ /** Releases the helper and all resources it is holding. */
public final T getManifest() { public void release() {
Assertions.checkNotNull(manifest); if (mediaPreparer != null) {
return manifest; mediaPreparer.release();
}
}
/**
* Returns the manifest, or null if no manifest is loaded. Must not be called until after
* preparation completes.
*/
@Nullable
public Object getManifest() {
if (mediaSource == null) {
return null;
}
assertPreparedWithMedia();
return mediaPreparer.manifest;
} }
/** /**
* Returns the number of periods for which media is available. Must not be called until after * Returns the number of periods for which media is available. Must not be called until after
* preparation completes. * preparation completes.
*/ */
public final int getPeriodCount() { public int getPeriodCount() {
Assertions.checkNotNull(trackGroupArrays); if (mediaSource == null) {
return 0;
}
assertPreparedWithMedia();
return trackGroupArrays.length; return trackGroupArrays.length;
} }
@ -198,8 +417,8 @@ public abstract class DownloadHelper<T> {
* @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream * @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream
* content. * content.
*/ */
public final TrackGroupArray getTrackGroups(int periodIndex) { public TrackGroupArray getTrackGroups(int periodIndex) {
Assertions.checkNotNull(trackGroupArrays); assertPreparedWithMedia();
return trackGroupArrays[periodIndex]; return trackGroupArrays[periodIndex];
} }
@ -210,8 +429,8 @@ public abstract class DownloadHelper<T> {
* @param periodIndex The period index. * @param periodIndex The period index.
* @return The {@link MappedTrackInfo} for the period. * @return The {@link MappedTrackInfo} for the period.
*/ */
public final MappedTrackInfo getMappedTrackInfo(int periodIndex) { public MappedTrackInfo getMappedTrackInfo(int periodIndex) {
Assertions.checkNotNull(mappedTrackInfos); assertPreparedWithMedia();
return mappedTrackInfos[periodIndex]; return mappedTrackInfos[periodIndex];
} }
@ -223,8 +442,8 @@ public abstract class DownloadHelper<T> {
* @param rendererIndex The renderer index. * @param rendererIndex The renderer index.
* @return A list of selected {@link TrackSelection track selections}. * @return A list of selected {@link TrackSelection track selections}.
*/ */
public final List<TrackSelection> getTrackSelections(int periodIndex, int rendererIndex) { public List<TrackSelection> getTrackSelections(int periodIndex, int rendererIndex) {
Assertions.checkNotNull(immutableTrackSelectionsByPeriodAndRenderer); assertPreparedWithMedia();
return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]; return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
} }
@ -234,8 +453,8 @@ public abstract class DownloadHelper<T> {
* *
* @param periodIndex The period index for which track selections are cleared. * @param periodIndex The period index for which track selections are cleared.
*/ */
public final void clearTrackSelections(int periodIndex) { public void clearTrackSelections(int periodIndex) {
Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer); assertPreparedWithMedia();
for (int i = 0; i < rendererCapabilities.length; i++) { for (int i = 0; i < rendererCapabilities.length; i++) {
trackSelectionsByPeriodAndRenderer[periodIndex][i].clear(); trackSelectionsByPeriodAndRenderer[periodIndex][i].clear();
} }
@ -249,7 +468,7 @@ public abstract class DownloadHelper<T> {
* @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
* selection of tracks. * selection of tracks.
*/ */
public final void replaceTrackSelections( public void replaceTrackSelections(
int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
clearTrackSelections(periodIndex); clearTrackSelections(periodIndex);
addTrackSelection(periodIndex, trackSelectorParameters); addTrackSelection(periodIndex, trackSelectorParameters);
@ -263,14 +482,71 @@ public abstract class DownloadHelper<T> {
* @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
* selection of tracks. * selection of tracks.
*/ */
public final void addTrackSelection( public void addTrackSelection(
int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
Assertions.checkNotNull(trackGroupArrays); assertPreparedWithMedia();
Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
trackSelector.setParameters(trackSelectorParameters); trackSelector.setParameters(trackSelectorParameters);
runTrackSelection(periodIndex); runTrackSelection(periodIndex);
} }
/**
* Convenience method to add selections of tracks for all specified audio languages. If an audio
* track in one of the specified languages is not available, the default fallback audio track is
* used instead. Must not be called until after preparation completes.
*
* @param languages A list of audio languages for which tracks should be added to the download
* selection, as ISO 639-1 two-letter or ISO 639-2 three-letter codes.
*/
public void addAudioLanguagesToSelection(String... languages) {
assertPreparedWithMedia();
for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) {
DefaultTrackSelector.ParametersBuilder parametersBuilder =
DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon();
MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex];
int rendererCount = mappedTrackInfo.getRendererCount();
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_AUDIO) {
parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true);
}
}
for (String language : languages) {
parametersBuilder.setPreferredAudioLanguage(language);
addTrackSelection(periodIndex, parametersBuilder.build());
}
}
}
/**
* Convenience method to add selections of tracks for all specified text languages. Must not be
* called until after preparation completes.
*
* @param selectUndeterminedTextLanguage Whether a text track with undetermined language should be
* selected for downloading if no track with one of the specified {@code languages} is
* available.
* @param languages A list of text languages for which tracks should be added to the download
* selection, as ISO 639-1 two-letter or ISO 639-2 three-letter codes.
*/
public void addTextLanguagesToSelection(
boolean selectUndeterminedTextLanguage, String... languages) {
assertPreparedWithMedia();
for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) {
DefaultTrackSelector.ParametersBuilder parametersBuilder =
DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon();
MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex];
int rendererCount = mappedTrackInfo.getRendererCount();
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_TEXT) {
parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true);
}
}
parametersBuilder.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage);
for (String language : languages) {
parametersBuilder.setPreferredTextLanguage(language);
addTrackSelection(periodIndex, parametersBuilder.build());
}
}
}
/** /**
* Builds a {@link DownloadAction} for downloading the selected tracks. Must not be called until * Builds a {@link DownloadAction} for downloading the selected tracks. Must not be called until
* after preparation completes. * after preparation completes.
@ -278,27 +554,22 @@ public abstract class DownloadHelper<T> {
* @param data Application provided data to store in {@link DownloadAction#data}. * @param data Application provided data to store in {@link DownloadAction#data}.
* @return The built {@link DownloadAction}. * @return The built {@link DownloadAction}.
*/ */
public final DownloadAction getDownloadAction(@Nullable byte[] data) { public DownloadAction getDownloadAction(@Nullable byte[] data) {
Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer); if (mediaSource == null) {
Assertions.checkNotNull(trackGroupArrays); return DownloadAction.createDownloadAction(
downloadType, uri, /* keys= */ Collections.emptyList(), cacheKey, data);
}
assertPreparedWithMedia();
List<StreamKey> streamKeys = new ArrayList<>(); List<StreamKey> streamKeys = new ArrayList<>();
List<TrackSelection> allSelections = new ArrayList<>();
int periodCount = trackSelectionsByPeriodAndRenderer.length; int periodCount = trackSelectionsByPeriodAndRenderer.length;
for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
allSelections.clear();
int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length; int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length;
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
List<TrackSelection> trackSelectionList = allSelections.addAll(trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]);
trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
for (int selectionIndex = 0; selectionIndex < trackSelectionList.size(); selectionIndex++) {
TrackSelection trackSelection = trackSelectionList.get(selectionIndex);
int trackGroupIndex =
trackGroupArrays[periodIndex].indexOf(trackSelection.getTrackGroup());
int trackCount = trackSelection.length();
for (int trackListIndex = 0; trackListIndex < trackCount; trackListIndex++) {
int trackIndex = trackSelection.getIndexInTrackGroup(trackListIndex);
streamKeys.add(toStreamKey(periodIndex, trackGroupIndex, trackIndex));
}
}
} }
streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections));
} }
return DownloadAction.createDownloadAction(downloadType, uri, streamKeys, cacheKey, data); return DownloadAction.createDownloadAction(downloadType, uri, streamKeys, cacheKey, data);
} }
@ -308,40 +579,18 @@ public abstract class DownloadHelper<T> {
* *
* @return The built {@link DownloadAction}. * @return The built {@link DownloadAction}.
*/ */
public final DownloadAction getRemoveAction() { public DownloadAction getRemoveAction() {
return DownloadAction.createRemoveAction(downloadType, uri, cacheKey); return DownloadAction.createRemoveAction(downloadType, uri, cacheKey);
} }
/** // Initialization of array of Lists.
* Loads the manifest. This method is called on a background thread.
*
* @param uri The manifest uri.
* @throws IOException If loading fails.
*/
protected abstract T loadManifest(Uri uri) throws IOException;
/**
* Returns the track group arrays for each period in the manifest.
*
* @param manifest The manifest.
* @return An array of {@link TrackGroupArray}s. One for each period in the manifest.
*/
protected abstract TrackGroupArray[] getTrackGroupArrays(T manifest);
/**
* Converts a track of a track group of a period to the corresponding {@link StreamKey}.
*
* @param periodIndex The index of the containing period.
* @param trackGroupIndex The index of the containing track group within the period.
* @param trackIndexInTrackGroup The index of the track within the track group.
* @return The corresponding {@link StreamKey}.
*/
protected abstract StreamKey toStreamKey(
int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@EnsuresNonNull("trackSelectionsByPeriodAndRenderer") private void onMediaPrepared() {
private void initializeTrackSelectionLists(int periodCount, int rendererCount) { Assertions.checkNotNull(mediaPreparer);
Assertions.checkNotNull(mediaPreparer.mediaPeriods);
Assertions.checkNotNull(mediaPreparer.timeline);
int periodCount = mediaPreparer.mediaPeriods.length;
int rendererCount = rendererCapabilities.length;
trackSelectionsByPeriodAndRenderer = trackSelectionsByPeriodAndRenderer =
(List<TrackSelection>[][]) new List<?>[periodCount][rendererCount]; (List<TrackSelection>[][]) new List<?>[periodCount][rendererCount];
immutableTrackSelectionsByPeriodAndRenderer = immutableTrackSelectionsByPeriodAndRenderer =
@ -353,6 +602,49 @@ public abstract class DownloadHelper<T> {
Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]); Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]);
} }
} }
trackGroupArrays = new TrackGroupArray[periodCount];
mappedTrackInfos = new MappedTrackInfo[periodCount];
for (int i = 0; i < periodCount; i++) {
trackGroupArrays[i] = mediaPreparer.mediaPeriods[i].getTrackGroups();
TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i);
trackSelector.onSelectionActivated(trackSelectorResult.info);
mappedTrackInfos[i] = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
}
setPreparedWithMedia();
Assertions.checkNotNull(callbackHandler)
.post(() -> Assertions.checkNotNull(callback).onPrepared(this));
}
private void onMediaPreparationFailed(IOException error) {
Assertions.checkNotNull(callbackHandler)
.post(() -> Assertions.checkNotNull(callback).onPrepareError(this, error));
}
@RequiresNonNull({
"trackGroupArrays",
"mappedTrackInfos",
"trackSelectionsByPeriodAndRenderer",
"immutableTrackSelectionsByPeriodAndRenderer",
"mediaPreparer",
"mediaPreparer.timeline",
"mediaPreparer.mediaPeriods"
})
private void setPreparedWithMedia() {
isPreparedWithMedia = true;
}
@EnsuresNonNull({
"trackGroupArrays",
"mappedTrackInfos",
"trackSelectionsByPeriodAndRenderer",
"immutableTrackSelectionsByPeriodAndRenderer",
"mediaPreparer",
"mediaPreparer.timeline",
"mediaPreparer.mediaPeriods"
})
@SuppressWarnings("nullness:contracts.postcondition.not.satisfied")
private void assertPreparedWithMedia() {
Assertions.checkState(isPreparedWithMedia);
} }
/** /**
@ -361,26 +653,27 @@ public abstract class DownloadHelper<T> {
*/ */
// Intentional reference comparison of track group instances. // Intentional reference comparison of track group instances.
@SuppressWarnings("ReferenceEquality") @SuppressWarnings("ReferenceEquality")
@RequiresNonNull({"trackGroupArrays", "trackSelectionsByPeriodAndRenderer"}) @RequiresNonNull({
"trackGroupArrays",
"trackSelectionsByPeriodAndRenderer",
"mediaPreparer",
"mediaPreparer.timeline"
})
private TrackSelectorResult runTrackSelection(int periodIndex) { private TrackSelectorResult runTrackSelection(int periodIndex) {
// TODO: Use actual timeline and media period id.
MediaPeriodId dummyMediaPeriodId = new MediaPeriodId(new Object());
Timeline dummyTimeline = Timeline.EMPTY;
currentTrackSelectionPeriodIndex = periodIndex;
try { try {
TrackSelectorResult trackSelectorResult = TrackSelectorResult trackSelectorResult =
trackSelector.selectTracks( trackSelector.selectTracks(
rendererCapabilities, rendererCapabilities,
trackGroupArrays[periodIndex], trackGroupArrays[periodIndex],
dummyMediaPeriodId, new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)),
dummyTimeline); mediaPreparer.timeline);
for (int i = 0; i < trackSelectorResult.length; i++) { for (int i = 0; i < trackSelectorResult.length; i++) {
TrackSelection newSelection = trackSelectorResult.selections.get(i); TrackSelection newSelection = trackSelectorResult.selections.get(i);
if (newSelection == null) { if (newSelection == null) {
continue; continue;
} }
List<TrackSelection> existingSelectionList = List<TrackSelection> existingSelectionList =
trackSelectionsByPeriodAndRenderer[currentTrackSelectionPeriodIndex][i]; trackSelectionsByPeriodAndRenderer[periodIndex][i];
boolean mergedWithExistingSelection = false; boolean mergedWithExistingSelection = false;
for (int j = 0; j < existingSelectionList.size(); j++) { for (int j = 0; j < existingSelectionList.size(); j++) {
TrackSelection existingSelection = existingSelectionList.get(j); TrackSelection existingSelection = existingSelectionList.get(j);
@ -414,6 +707,156 @@ public abstract class DownloadHelper<T> {
} }
} }
private static Pair<@NullableType Constructor<?>, @NullableType Method>
getMediaSourceFactoryMethods(String className) {
Constructor<?> constructor = null;
Method createMethod = null;
try {
// LINT.IfChange
Class<?> factoryClazz = Class.forName(className);
constructor = factoryClazz.getConstructor(DataSource.Factory.class);
createMethod = factoryClazz.getMethod("createMediaSource", Uri.class);
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
} catch (Exception e) {
// Expected if the app was built without the respective module.
}
return Pair.create(constructor, createMethod);
}
private static MediaSource createMediaSource(
Uri uri,
DataSource.Factory dataSourceFactory,
@Nullable Constructor<?> factoryConstructor,
@Nullable Method createMediaSourceMethod) {
if (factoryConstructor == null || createMediaSourceMethod == null) {
throw new IllegalStateException("Module missing to create media source.");
}
try {
Object factory = factoryConstructor.newInstance(dataSourceFactory);
return (MediaSource) Assertions.checkNotNull(createMediaSourceMethod.invoke(factory, uri));
} catch (Exception e) {
throw new IllegalStateException("Failed to instantiate media source.", e);
}
}
private static final class MediaPreparer
implements MediaSource.SourceInfoRefreshListener, MediaPeriod.Callback, Handler.Callback {
private static final int MESSAGE_PREPARE_SOURCE = 0;
private static final int MESSAGE_CHECK_FOR_FAILURE = 1;
private static final int MESSAGE_CONTINUE_LOADING = 2;
private final MediaSource mediaSource;
private final DownloadHelper downloadHelper;
private final Allocator allocator;
private final HandlerThread mediaSourceThread;
private final Handler mediaSourceHandler;
@Nullable public Object manifest;
public @MonotonicNonNull Timeline timeline;
public MediaPeriod @MonotonicNonNull [] mediaPeriods;
private final ArrayList<MediaPeriod> pendingMediaPeriods;
public MediaPreparer(MediaSource mediaSource, DownloadHelper downloadHelper) {
this.mediaSource = mediaSource;
this.downloadHelper = downloadHelper;
allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
mediaSourceThread = new HandlerThread("DownloadHelper");
mediaSourceThread.start();
mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this);
mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE);
pendingMediaPeriods = new ArrayList<>();
}
public void release() {
if (mediaPeriods != null) {
for (MediaPeriod mediaPeriod : mediaPeriods) {
mediaSource.releasePeriod(mediaPeriod);
}
}
mediaSource.releaseSource(this);
mediaSourceThread.quit();
}
// Handler.Callback
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_PREPARE_SOURCE:
mediaSource.prepareSource(/* listener= */ this, /* mediaTransferListener= */ null);
mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE);
return true;
case MESSAGE_CHECK_FOR_FAILURE:
try {
if (mediaPeriods == null) {
mediaSource.maybeThrowSourceInfoRefreshError();
} else {
for (int i = 0; i < pendingMediaPeriods.size(); i++) {
pendingMediaPeriods.get(i).maybeThrowPrepareError();
}
}
mediaSourceHandler.sendEmptyMessageDelayed(
MESSAGE_CHECK_FOR_FAILURE, /* delayMillis= */ 100);
} catch (IOException e) {
downloadHelper.onMediaPreparationFailed(e);
}
return true;
case MESSAGE_CONTINUE_LOADING:
MediaPeriod mediaPeriod = (MediaPeriod) msg.obj;
if (pendingMediaPeriods.contains(mediaPeriod)) {
mediaPeriod.continueLoading(/* positionUs= */ 0);
}
return true;
default:
return false;
}
}
// MediaSource.SourceInfoRefreshListener implementation.
@Override
public void onSourceInfoRefreshed(
MediaSource source, Timeline timeline, @Nullable Object manifest) {
if (this.timeline != null) {
// Ignore dynamic updates.
return;
}
this.timeline = timeline;
this.manifest = manifest;
mediaPeriods = new MediaPeriod[timeline.getPeriodCount()];
for (int i = 0; i < mediaPeriods.length; i++) {
MediaPeriod mediaPeriod =
mediaSource.createPeriod(
new MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ i)),
allocator,
/* startPositionUs= */ 0);
mediaPeriods[i] = mediaPeriod;
pendingMediaPeriods.add(mediaPeriod);
mediaPeriod.prepare(/* callback= */ this, /* positionUs= */ 0);
}
}
// MediaPeriod.Callback implementation.
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
pendingMediaPeriods.remove(mediaPeriod);
if (pendingMediaPeriods.isEmpty()) {
mediaSourceHandler.removeMessages(MESSAGE_CHECK_FOR_FAILURE);
downloadHelper.onMediaPrepared();
}
}
@Override
public void onContinueLoadingRequested(MediaPeriod mediaPeriod) {
if (pendingMediaPeriods.contains(mediaPeriod)) {
mediaSourceHandler.obtainMessage(MESSAGE_CONTINUE_LOADING, mediaPeriod).sendToTarget();
}
}
}
private static final class DownloadTrackSelection extends BaseTrackSelection { private static final class DownloadTrackSelection extends BaseTrackSelection {
private static final class Factory implements TrackSelection.Factory { private static final class Factory implements TrackSelection.Factory {

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -25,21 +25,28 @@ import static com.google.android.exoplayer2.offline.DownloadState.STATE_REMOVED;
import static com.google.android.exoplayer2.offline.DownloadState.STATE_REMOVING; import static com.google.android.exoplayer2.offline.DownloadState.STATE_REMOVING;
import static com.google.android.exoplayer2.offline.DownloadState.STATE_RESTARTING; import static com.google.android.exoplayer2.offline.DownloadState.STATE_RESTARTING;
import static com.google.android.exoplayer2.offline.DownloadState.STATE_STOPPED; import static com.google.android.exoplayer2.offline.DownloadState.STATE_STOPPED;
import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY; import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_MANUAL;
import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_STOPPED; import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_REQUIREMENTS_NOT_MET;
import android.content.Context;
import android.os.ConditionVariable; import android.os.ConditionVariable;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.RequirementsWatcher;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -74,22 +81,55 @@ public final class DownloadManager {
* @param downloadManager The reporting instance. * @param downloadManager The reporting instance.
*/ */
void onIdle(DownloadManager downloadManager); void onIdle(DownloadManager downloadManager);
/**
* Called when the download requirements state changed.
*
* @param downloadManager The reporting instance.
* @param requirements Requirements needed to be met to start downloads.
* @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not
* met, or 0.
*/
void onRequirementsStateChanged(
DownloadManager downloadManager,
Requirements requirements,
@Requirements.RequirementFlags int notMetRequirements);
} }
/** The default maximum number of simultaneous downloads. */ /** The default maximum number of simultaneous downloads. */
public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1; public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1;
/** The default minimum number of times a download must be retried before failing. */ /** The default minimum number of times a download must be retried before failing. */
public static final int DEFAULT_MIN_RETRY_COUNT = 5; public static final int DEFAULT_MIN_RETRY_COUNT = 5;
/** The default requirement is that the device has network connectivity. */
public static final Requirements DEFAULT_REQUIREMENTS =
new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
@Retention(RetentionPolicy.SOURCE)
@IntDef({
START_THREAD_SUCCEEDED,
START_THREAD_WAIT_REMOVAL_TO_FINISH,
START_THREAD_WAIT_DOWNLOAD_CANCELLATION,
START_THREAD_TOO_MANY_DOWNLOADS,
START_THREAD_NOT_ALLOWED
})
private @interface StartThreadResults {}
private static final int START_THREAD_SUCCEEDED = 0;
private static final int START_THREAD_WAIT_REMOVAL_TO_FINISH = 1;
private static final int START_THREAD_WAIT_DOWNLOAD_CANCELLATION = 2;
private static final int START_THREAD_TOO_MANY_DOWNLOADS = 3;
private static final int START_THREAD_NOT_ALLOWED = 4;
private static final String TAG = "DownloadManager"; private static final String TAG = "DownloadManager";
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
private final int maxActiveDownloads; private final int maxSimultaneousDownloads;
private final int minRetryCount; private final int minRetryCount;
private final Context context;
private final ActionFile actionFile; private final ActionFile actionFile;
private final DownloaderFactory downloaderFactory; private final DownloaderFactory downloaderFactory;
private final ArrayList<Download> downloads; private final ArrayList<Download> downloads;
private final ArrayList<Download> activeDownloads; private final HashMap<Download, DownloadThread> activeDownloads;
private final Handler handler; private final Handler handler;
private final HandlerThread fileIOThread; private final HandlerThread fileIOThread;
private final Handler fileIOHandler; private final Handler fileIOHandler;
@ -98,40 +138,55 @@ public final class DownloadManager {
private boolean initialized; private boolean initialized;
private boolean released; private boolean released;
@DownloadState.StopFlags private int stickyStopFlags; @DownloadState.StopFlags private int stopFlags;
@Requirements.RequirementFlags private int notMetRequirements;
private int manualStopReason;
private RequirementsWatcher requirementsWatcher;
private int simultaneousDownloads;
/** /**
* Constructs a {@link DownloadManager}. * Constructs a {@link DownloadManager}.
* *
* @param context Any context.
* @param actionFile The file in which active actions are saved. * @param actionFile The file in which active actions are saved.
* @param downloaderFactory A factory for creating {@link Downloader}s. * @param downloaderFactory A factory for creating {@link Downloader}s.
*/ */
public DownloadManager(File actionFile, DownloaderFactory downloaderFactory) { public DownloadManager(Context context, File actionFile, DownloaderFactory downloaderFactory) {
this( this(
actionFile, downloaderFactory, DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, DEFAULT_MIN_RETRY_COUNT); context,
actionFile,
downloaderFactory,
DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS,
DEFAULT_MIN_RETRY_COUNT,
DEFAULT_REQUIREMENTS);
} }
/** /**
* Constructs a {@link DownloadManager}. * Constructs a {@link DownloadManager}.
* *
* @param context Any context.
* @param actionFile The file in which active actions are saved. * @param actionFile The file in which active actions are saved.
* @param downloaderFactory A factory for creating {@link Downloader}s. * @param downloaderFactory A factory for creating {@link Downloader}s.
* @param maxSimultaneousDownloads The maximum number of simultaneous downloads. * @param maxSimultaneousDownloads The maximum number of simultaneous downloads.
* @param minRetryCount The minimum number of times a download must be retried before failing. * @param minRetryCount The minimum number of times a download must be retried before failing.
* @param requirements The requirements needed to be met to start downloads.
*/ */
public DownloadManager( public DownloadManager(
Context context,
File actionFile, File actionFile,
DownloaderFactory downloaderFactory, DownloaderFactory downloaderFactory,
int maxSimultaneousDownloads, int maxSimultaneousDownloads,
int minRetryCount) { int minRetryCount,
Requirements requirements) {
this.context = context.getApplicationContext();
this.actionFile = new ActionFile(actionFile); this.actionFile = new ActionFile(actionFile);
this.downloaderFactory = downloaderFactory; this.downloaderFactory = downloaderFactory;
this.maxActiveDownloads = maxSimultaneousDownloads; this.maxSimultaneousDownloads = maxSimultaneousDownloads;
this.minRetryCount = minRetryCount; this.minRetryCount = minRetryCount;
this.stickyStopFlags = STOP_FLAG_STOPPED | STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY;
stopFlags = STOP_FLAG_MANUAL;
downloads = new ArrayList<>(); downloads = new ArrayList<>();
activeDownloads = new ArrayList<>(); activeDownloads = new HashMap<>();
Looper looper = Looper.myLooper(); Looper looper = Looper.myLooper();
if (looper == null) { if (looper == null) {
@ -146,10 +201,30 @@ public final class DownloadManager {
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
actionQueue = new ArrayDeque<>(); actionQueue = new ArrayDeque<>();
setNotMetRequirements(watchRequirements(requirements));
loadActions(); loadActions();
logd("Created"); logd("Created");
} }
/**
* Sets the requirements needed to be met to start downloads.
*
* @param requirements Need to be met to start downloads.
*/
public void setRequirements(Requirements requirements) {
Assertions.checkState(!released);
if (requirements.equals(requirementsWatcher.getRequirements())) {
return;
}
requirementsWatcher.stop();
onRequirementsStateChanged(watchRequirements(requirements));
}
/** Returns the requirements needed to be met to start downloads. */
public Requirements getRequirements() {
return requirementsWatcher.getRequirements();
}
/** /**
* Adds a {@link Listener}. * Adds a {@link Listener}.
* *
@ -168,33 +243,35 @@ public final class DownloadManager {
listeners.remove(listener); listeners.remove(listener);
} }
/** Starts the downloads. */ /**
* Clears {@link DownloadState#STOP_FLAG_MANUAL} flag of all downloads. Downloads are started if
* the requirements are met.
*/
public void startDownloads() { public void startDownloads() {
clearStopFlags(STOP_FLAG_STOPPED); logd("manual stopped is cancelled");
} manualStopReason = 0;
stopFlags &= ~STOP_FLAG_MANUAL;
/** Stops all of the downloads. Call {@link #startDownloads()} to restart downloads. */
public void stopDownloads() {
setStopFlags(STOP_FLAG_STOPPED);
}
private void setStopFlags(int flags) {
updateStopFlags(flags, flags);
}
private void clearStopFlags(int flags) {
updateStopFlags(flags, 0);
}
private void updateStopFlags(int flags, int values) {
Assertions.checkState(!released);
int updatedStickyStopFlags = (values & flags) | (stickyStopFlags & ~flags);
if (stickyStopFlags != updatedStickyStopFlags) {
stickyStopFlags = updatedStickyStopFlags;
for (int i = 0; i < downloads.size(); i++) { for (int i = 0; i < downloads.size(); i++) {
downloads.get(i).updateStopFlags(flags, values); downloads.get(i).clearManualStopReason();
} }
logdFlags("Sticky stop flags are updated", updatedStickyStopFlags); }
/** Signals all downloads to stop. Call {@link #startDownloads()} to let them to be started. */
public void stopDownloads() {
stopDownloads(0);
}
/**
* Signals all downloads to stop. Call {@link #startDownloads()} to let them to be started.
*
* @param manualStopReason An application defined stop reason.
*/
public void stopDownloads(int manualStopReason) {
logd("downloads are stopped manually");
this.manualStopReason = manualStopReason;
stopFlags |= STOP_FLAG_MANUAL;
for (int i = 0; i < downloads.size(); i++) {
downloads.get(i).setManualStopReason(this.manualStopReason);
} }
} }
@ -256,15 +333,7 @@ public final class DownloadManager {
/** Returns whether there are no active downloads. */ /** Returns whether there are no active downloads. */
public boolean isIdle() { public boolean isIdle() {
Assertions.checkState(!released); Assertions.checkState(!released);
if (!initialized) { return initialized && activeDownloads.isEmpty();
return false;
}
for (int i = 0; i < downloads.size(); i++) {
if (!downloads.get(i).isIdle()) {
return false;
}
}
return true;
} }
/** /**
@ -276,8 +345,11 @@ public final class DownloadManager {
if (released) { if (released) {
return; return;
} }
setStopFlags(STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY);
released = true; released = true;
stopAllDownloadThreads();
if (requirementsWatcher != null) {
requirementsWatcher.stop();
}
final ConditionVariable fileIOFinishedCondition = new ConditionVariable(); final ConditionVariable fileIOFinishedCondition = new ConditionVariable();
fileIOHandler.post(fileIOFinishedCondition::open); fileIOHandler.post(fileIOFinishedCondition::open);
fileIOFinishedCondition.block(); fileIOFinishedCondition.block();
@ -293,20 +365,11 @@ public final class DownloadManager {
return; return;
} }
} }
Download download = Download download = new Download(this, action, stopFlags, notMetRequirements, manualStopReason);
new Download(this, downloaderFactory, action, minRetryCount, stickyStopFlags);
downloads.add(download); downloads.add(download);
logd("Download is added", download); logd("Download is added", download);
} }
private void maybeStartDownload(Download download) {
if (activeDownloads.size() < maxActiveDownloads) {
if (download.start()) {
activeDownloads.add(download);
}
}
}
private void maybeNotifyListenersIdle() { private void maybeNotifyListenersIdle() {
if (!isIdle()) { if (!isIdle()) {
return; return;
@ -321,21 +384,11 @@ public final class DownloadManager {
if (released) { if (released) {
return; return;
} }
boolean idle = download.isIdle();
if (idle) {
activeDownloads.remove(download);
}
notifyListenersDownloadStateChange(download); notifyListenersDownloadStateChange(download);
if (download.isFinished()) { if (download.isFinished()) {
downloads.remove(download); downloads.remove(download);
saveActions(); saveActions();
} }
if (idle) {
for (int i = 0; i < downloads.size(); i++) {
maybeStartDownload(downloads.get(i));
}
maybeNotifyListenersIdle();
}
} }
private void notifyListenersDownloadStateChange(Download download) { private void notifyListenersDownloadStateChange(Download download) {
@ -346,6 +399,27 @@ public final class DownloadManager {
} }
} }
private void onRequirementsStateChanged(@Requirements.RequirementFlags int notMetRequirements) {
setNotMetRequirements(notMetRequirements);
logdFlags("Not met requirements are changed", notMetRequirements);
Requirements requirements = requirementsWatcher.getRequirements();
for (Listener listener : listeners) {
listener.onRequirementsStateChanged(DownloadManager.this, requirements, notMetRequirements);
}
for (int i = 0; i < downloads.size(); i++) {
downloads.get(i).setNotMetRequirements(notMetRequirements);
}
}
private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) {
this.notMetRequirements = notMetRequirements;
if (notMetRequirements == 0) {
stopFlags &= ~STOP_FLAG_REQUIREMENTS_NOT_MET;
} else {
stopFlags |= STOP_FLAG_REQUIREMENTS_NOT_MET;
}
}
private void loadActions() { private void loadActions() {
fileIOHandler.post( fileIOHandler.post(
() -> { () -> {
@ -377,7 +451,9 @@ public final class DownloadManager {
for (Listener listener : listeners) { for (Listener listener : listeners) {
listener.onInitialized(DownloadManager.this); listener.onInitialized(DownloadManager.this);
} }
clearStopFlags(STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY); for (int i = 0; i < downloads.size(); i++) {
downloads.get(i).start();
}
}); });
}); });
} }
@ -420,37 +496,125 @@ public final class DownloadManager {
} }
} }
@Requirements.RequirementFlags
private int watchRequirements(Requirements requirements) {
RequirementsWatcher.Listener listener =
(requirementsWatcher, notMetRequirements) -> onRequirementsStateChanged(notMetRequirements);
requirementsWatcher = new RequirementsWatcher(context, listener, requirements);
@Requirements.RequirementFlags int notMetRequirements = requirementsWatcher.start();
if (notMetRequirements == 0) {
startDownloads();
} else {
stopDownloads();
}
return notMetRequirements;
}
@StartThreadResults
private int startDownloadThread(Download download, DownloadAction action) {
if (!initialized || released) {
return START_THREAD_NOT_ALLOWED;
}
if (activeDownloads.containsKey(download)) {
if (stopDownloadThread(download)) {
return START_THREAD_WAIT_DOWNLOAD_CANCELLATION;
}
return START_THREAD_WAIT_REMOVAL_TO_FINISH;
}
if (!action.isRemoveAction) {
if (simultaneousDownloads == maxSimultaneousDownloads) {
return START_THREAD_TOO_MANY_DOWNLOADS;
}
simultaneousDownloads++;
}
Downloader downloader = downloaderFactory.createDownloader(action);
DownloadThread downloadThread = new DownloadThread(download, downloader, action.isRemoveAction);
activeDownloads.put(download, downloadThread);
logd("Download is started", download);
return START_THREAD_SUCCEEDED;
}
private boolean stopDownloadThread(Download download) {
DownloadThread downloadThread = activeDownloads.get(download);
if (downloadThread != null && !downloadThread.isRemoveThread) {
downloadThread.cancel();
logd("Download is cancelled", download);
return true;
}
return false;
}
private void stopAllDownloadThreads() {
for (Download download : activeDownloads.keySet()) {
stopDownloadThread(download);
}
}
private void onDownloadThreadStopped(DownloadThread downloadThread, Throwable finalError) {
Download download = downloadThread.download;
logd("Download is stopped", download);
activeDownloads.remove(download);
boolean tryToStartDownloads = false;
if (!downloadThread.isRemoveThread) {
// If maxSimultaneousDownloads was hit, there might be a download waiting for a slot.
tryToStartDownloads = simultaneousDownloads == maxSimultaneousDownloads;
simultaneousDownloads--;
}
download.onDownloadThreadStopped(downloadThread.isCanceled, finalError);
if (tryToStartDownloads) {
for (int i = 0;
simultaneousDownloads < maxSimultaneousDownloads && i < downloads.size();
i++) {
downloads.get(i).start();
}
}
maybeNotifyListenersIdle();
}
@Nullable
private Downloader getDownloader(Download download) {
DownloadThread downloadThread = activeDownloads.get(download);
if (downloadThread != null) {
return downloadThread.downloader;
}
return null;
}
private static final class Download { private static final class Download {
private final String id; private final String id;
private final DownloadManager downloadManager; private final DownloadManager downloadManager;
private final DownloaderFactory downloaderFactory;
private final int minRetryCount;
private final long startTimeMs; private final long startTimeMs;
private final ArrayDeque<DownloadAction> actionQueue; private final ArrayDeque<DownloadAction> actionQueue;
/** The current state of the download. */
@DownloadState.State private int state;
@MonotonicNonNull private Downloader downloader; @DownloadState.State private int state;
@MonotonicNonNull private DownloadThread downloadThread;
@MonotonicNonNull @DownloadState.FailureReason private int failureReason; @MonotonicNonNull @DownloadState.FailureReason private int failureReason;
@DownloadState.StopFlags private int stopFlags; @DownloadState.StopFlags private int stopFlags;
@Requirements.RequirementFlags private int notMetRequirements;
private int manualStopReason;
private Download( private Download(
DownloadManager downloadManager, DownloadManager downloadManager,
DownloaderFactory downloaderFactory,
DownloadAction action, DownloadAction action,
int minRetryCount, @DownloadState.StopFlags int stopFlags,
int stopFlags) { @Requirements.RequirementFlags int notMetRequirements,
int manualStopReason) {
this.id = action.id; this.id = action.id;
this.downloadManager = downloadManager; this.downloadManager = downloadManager;
this.downloaderFactory = downloaderFactory; this.notMetRequirements = notMetRequirements;
this.minRetryCount = minRetryCount; this.manualStopReason = manualStopReason;
this.stopFlags = stopFlags; this.stopFlags = stopFlags;
this.startTimeMs = System.currentTimeMillis(); this.startTimeMs = System.currentTimeMillis();
actionQueue = new ArrayDeque<>(); actionQueue = new ArrayDeque<>();
actionQueue.add(action); actionQueue.add(action);
initialize(/* restart= */ false);
// Set to queued state but don't notify listeners until we make sure we don't switch to
// another state immediately.
state = STATE_QUEUED;
initialize();
if (state == STATE_QUEUED) {
downloadManager.onDownloadStateChange(this);
}
} }
public boolean addAction(DownloadAction newAction) { public boolean addAction(DownloadAction newAction) {
@ -472,12 +636,9 @@ public final class DownloadManager {
setState(STATE_REMOVING); setState(STATE_REMOVING);
} }
} else if (!action.equals(updatedAction)) { } else if (!action.equals(updatedAction)) {
if (state == STATE_DOWNLOADING) { Assertions.checkState(
stopDownloadThread(); state == STATE_DOWNLOADING || state == STATE_QUEUED || state == STATE_STOPPED);
} else { initialize();
Assertions.checkState(state == STATE_QUEUED || state == STATE_STOPPED);
initialize(/* restart= */ false);
}
} }
return true; return true;
} }
@ -486,6 +647,7 @@ public final class DownloadManager {
float downloadPercentage = C.PERCENTAGE_UNSET; float downloadPercentage = C.PERCENTAGE_UNSET;
long downloadedBytes = 0; long downloadedBytes = 0;
long totalBytes = C.LENGTH_UNSET; long totalBytes = C.LENGTH_UNSET;
Downloader downloader = downloadManager.getDownloader(this);
if (downloader != null) { if (downloader != null) {
downloadPercentage = downloader.getDownloadPercentage(); downloadPercentage = downloader.getDownloadPercentage();
downloadedBytes = downloader.getDownloadedBytes(); downloadedBytes = downloader.getDownloadedBytes();
@ -503,6 +665,8 @@ public final class DownloadManager {
totalBytes, totalBytes,
failureReason, failureReason,
stopFlags, stopFlags,
notMetRequirements,
manualStopReason,
startTimeMs, startTimeMs,
/* updateTimeMs= */ System.currentTimeMillis(), /* updateTimeMs= */ System.currentTimeMillis(),
action.keys.toArray(new StreamKey[0]), action.keys.toArray(new StreamKey[0]),
@ -522,83 +686,89 @@ public final class DownloadManager {
return id + ' ' + DownloadState.getStateString(state); return id + ' ' + DownloadState.getStateString(state);
} }
public boolean start() { public void start() {
if (state != STATE_QUEUED) { if (state == STATE_QUEUED || state == STATE_DOWNLOADING) {
return false; startOrQueue();
} else if (state == STATE_REMOVING || state == STATE_RESTARTING) {
downloadManager.startDownloadThread(this, actionQueue.peek());
} }
startDownloadThread(actionQueue.peek());
setState(STATE_DOWNLOADING);
return true;
} }
public void setStopFlags(int flags) { public void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) {
updateStopFlags(flags, flags); this.notMetRequirements = notMetRequirements;
updateStopFlags(STOP_FLAG_REQUIREMENTS_NOT_MET, /* setFlags= */ notMetRequirements != 0);
} }
public void clearStopFlags(int flags) { public void setManualStopReason(int manualStopReason) {
updateStopFlags(flags, 0); this.manualStopReason = manualStopReason;
updateStopFlags(STOP_FLAG_MANUAL, /* setFlags= */ true);
} }
public void updateStopFlags(int flags, int values) { public void clearManualStopReason() {
stopFlags = (values & flags) | (stopFlags & ~flags); this.manualStopReason = 0;
updateStopFlags(STOP_FLAG_MANUAL, /* setFlags= */ false);
}
private void updateStopFlags(int flags, boolean setFlags) {
if (setFlags) {
stopFlags |= flags;
} else {
stopFlags &= ~flags;
}
if (stopFlags != 0) { if (stopFlags != 0) {
if (state == STATE_DOWNLOADING) { if (state == STATE_DOWNLOADING || state == STATE_QUEUED) {
stopDownloadThread(); downloadManager.stopDownloadThread(this);
} else if (state == STATE_QUEUED) {
setState(STATE_STOPPED); setState(STATE_STOPPED);
} }
} else if (state == STATE_STOPPED) { } else if (state == STATE_STOPPED) {
startOrQueue(/* restart= */ false); startOrQueue();
} }
} }
private void initialize(boolean restart) { private void initialize() {
DownloadAction action = actionQueue.peek(); DownloadAction action = actionQueue.peek();
if (action.isRemoveAction) { if (action.isRemoveAction) {
if (!downloadManager.released) { int result = downloadManager.startDownloadThread(this, action);
startDownloadThread(action); Assertions.checkState(
} result == START_THREAD_SUCCEEDED
|| result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION
|| result == START_THREAD_NOT_ALLOWED);
setState(actionQueue.size() == 1 ? STATE_REMOVING : STATE_RESTARTING); setState(actionQueue.size() == 1 ? STATE_REMOVING : STATE_RESTARTING);
} else if (stopFlags != 0) { } else if (stopFlags != 0) {
setState(STATE_STOPPED); setState(STATE_STOPPED);
} else { } else {
startOrQueue(restart); startOrQueue();
} }
} }
private void startOrQueue(boolean restart) { private void startOrQueue() {
// Set to queued state but don't notify listeners until we make sure we can't start now. DownloadAction action = Assertions.checkNotNull(actionQueue.peek());
state = STATE_QUEUED; Assertions.checkState(!action.isRemoveAction);
if (restart) { @StartThreadResults int result = downloadManager.startDownloadThread(this, action);
start(); Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH);
if (result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION) {
setState(STATE_DOWNLOADING);
} else { } else {
downloadManager.maybeStartDownload(this); setState(STATE_QUEUED);
}
if (state == STATE_QUEUED) {
downloadManager.onDownloadStateChange(this);
} }
} }
private void setState(@DownloadState.State int newState) { private void setState(@DownloadState.State int newState) {
if (state != newState) {
state = newState; state = newState;
downloadManager.onDownloadStateChange(this); downloadManager.onDownloadStateChange(this);
} }
private void startDownloadThread(DownloadAction action) {
downloader = downloaderFactory.createDownloader(action);
downloadThread =
new DownloadThread(
this, downloader, action.isRemoveAction, minRetryCount, downloadManager.handler);
} }
private void stopDownloadThread() { private void onDownloadThreadStopped(boolean isCanceled, @Nullable Throwable error) {
Assertions.checkNotNull(downloadThread).cancel();
}
private void onDownloadThreadStopped(@Nullable Throwable finalError) {
failureReason = FAILURE_REASON_NONE; failureReason = FAILURE_REASON_NONE;
if (!downloadThread.isCanceled) { if (isCanceled) {
if (finalError != null && state != STATE_REMOVING && state != STATE_RESTARTING) { if (!isIdle()) {
downloadManager.startDownloadThread(this, actionQueue.peek());
}
return;
}
if (error != null && state == STATE_DOWNLOADING) {
failureReason = FAILURE_REASON_UNKNOWN; failureReason = FAILURE_REASON_UNKNOWN;
setState(STATE_FAILED); setState(STATE_FAILED);
return; return;
@ -613,32 +783,22 @@ public final class DownloadManager {
return; return;
} }
actionQueue.remove(); actionQueue.remove();
} initialize();
initialize(/* restart= */ state == STATE_DOWNLOADING);
} }
} }
private static class DownloadThread implements Runnable { private class DownloadThread implements Runnable {
private final Download download; private final Download download;
private final Downloader downloader; private final Downloader downloader;
private final boolean remove; private final boolean isRemoveThread;
private final int minRetryCount;
private final Handler callbackHandler;
private final Thread thread; private final Thread thread;
private volatile boolean isCanceled; private volatile boolean isCanceled;
private DownloadThread( private DownloadThread(Download download, Downloader downloader, boolean isRemoveThread) {
Download download,
Downloader downloader,
boolean remove,
int minRetryCount,
Handler callbackHandler) {
this.download = download; this.download = download;
this.downloader = downloader; this.downloader = downloader;
this.remove = remove; this.isRemoveThread = isRemoveThread;
this.minRetryCount = minRetryCount;
this.callbackHandler = callbackHandler;
thread = new Thread(this); thread = new Thread(this);
thread.start(); thread.start();
} }
@ -653,10 +813,10 @@ public final class DownloadManager {
@Override @Override
public void run() { public void run() {
logd("Download is started", download); logd("Download started", download);
Throwable error = null; Throwable error = null;
try { try {
if (remove) { if (isRemoveThread) {
downloader.remove(); downloader.remove();
} else { } else {
int errorCount = 0; int errorCount = 0;
@ -686,11 +846,12 @@ public final class DownloadManager {
error = e; error = e;
} }
final Throwable finalError = error; final Throwable finalError = error;
callbackHandler.post(() -> download.onDownloadThreadStopped(isCanceled ? null : finalError)); handler.post(() -> onDownloadThreadStopped(this, finalError));
} }
private int getRetryDelayMillis(int errorCount) { private int getRetryDelayMillis(int errorCount) {
return Math.min((errorCount - 1) * 1000, 5000); return Math.min((errorCount - 1) * 1000, 5000);
} }
} }
} }

View File

@ -25,8 +25,8 @@ import android.os.Looper;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.RequirementsWatcher;
import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.scheduler.Scheduler;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.NotificationUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
@ -43,10 +43,6 @@ public abstract class DownloadService extends Service {
/** Starts a download service, adding a new {@link DownloadAction} to be executed. */ /** Starts a download service, adding a new {@link DownloadAction} to be executed. */
public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD"; public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD";
/** Reloads the download requirements. */
public static final String ACTION_RELOAD_REQUIREMENTS =
"com.google.android.exoplayer.downloadService.action.RELOAD_REQUIREMENTS";
/** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */ /** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */
private static final String ACTION_RESTART = private static final String ACTION_RESTART =
"com.google.android.exoplayer.downloadService.action.RESTART"; "com.google.android.exoplayer.downloadService.action.RESTART";
@ -70,20 +66,16 @@ public abstract class DownloadService extends Service {
private static final String TAG = "DownloadService"; private static final String TAG = "DownloadService";
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
// Keep the requirements helper for each DownloadService as long as there are downloads (and the // Keep DownloadManagerListeners for each DownloadService as long as there are downloads (and the
// process is running). This allows downloads to resume when there's no scheduler. It may also // process is running). This allows DownloadService to restart when there's no scheduler.
// allow downloads the resume more quickly than when relying on the scheduler alone. private static final HashMap<Class<? extends DownloadService>, DownloadManagerHelper>
private static final HashMap<Class<? extends DownloadService>, RequirementsHelper> downloadManagerListeners = new HashMap<>();
requirementsHelpers = new HashMap<>();
private static final Requirements DEFAULT_REQUIREMENTS =
new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
private final @Nullable ForegroundNotificationUpdater foregroundNotificationUpdater; private final @Nullable ForegroundNotificationUpdater foregroundNotificationUpdater;
private final @Nullable String channelId; private final @Nullable String channelId;
private final @StringRes int channelName; private final @StringRes int channelName;
private DownloadManager downloadManager; private DownloadManager downloadManager;
private DownloadManagerListener downloadManagerListener;
private int lastStartId; private int lastStartId;
private boolean startedInForeground; private boolean startedInForeground;
private boolean taskRemoved; private boolean taskRemoved;
@ -227,9 +219,16 @@ public abstract class DownloadService extends Service {
NotificationUtil.createNotificationChannel( NotificationUtil.createNotificationChannel(
this, channelId, channelName, NotificationUtil.IMPORTANCE_LOW); this, channelId, channelName, NotificationUtil.IMPORTANCE_LOW);
} }
downloadManager = getDownloadManager(); Class<? extends DownloadService> clazz = getClass();
downloadManagerListener = new DownloadManagerListener(); DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz);
downloadManager.addListener(downloadManagerListener); if (downloadManagerHelper == null) {
downloadManagerHelper =
new DownloadManagerHelper(
getApplicationContext(), getDownloadManager(), getScheduler(), clazz);
downloadManagerListeners.put(clazz, downloadManagerHelper);
}
downloadManager = downloadManagerHelper.downloadManager;
downloadManagerHelper.attachService(this);
} }
@Override @Override
@ -264,22 +263,11 @@ public abstract class DownloadService extends Service {
} }
} }
break; break;
case ACTION_RELOAD_REQUIREMENTS:
stopWatchingRequirements();
break;
default: default:
Log.e(TAG, "Ignoring unrecognized action: " + intentAction); Log.e(TAG, "Ignoring unrecognized action: " + intentAction);
break; break;
} }
Requirements requirements = getRequirements();
if (requirements.checkRequirements(this)) {
downloadManager.startDownloads();
} else {
downloadManager.stopDownloads();
}
maybeStartWatchingRequirements(requirements);
if (downloadManager.isIdle()) { if (downloadManager.isIdle()) {
stop(); stop();
} }
@ -295,11 +283,12 @@ public abstract class DownloadService extends Service {
@Override @Override
public void onDestroy() { public void onDestroy() {
logd("onDestroy"); logd("onDestroy");
DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(getClass());
boolean unschedule = downloadManager.getDownloadCount() <= 0;
downloadManagerHelper.detachService(this, unschedule);
if (foregroundNotificationUpdater != null) { if (foregroundNotificationUpdater != null) {
foregroundNotificationUpdater.stopPeriodicUpdates(); foregroundNotificationUpdater.stopPeriodicUpdates();
} }
downloadManager.removeListener(downloadManagerListener);
maybeStopWatchingRequirements();
} }
/** DownloadService isn't designed to be bound. */ /** DownloadService isn't designed to be bound. */
@ -311,9 +300,7 @@ public abstract class DownloadService extends Service {
/** /**
* Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the * Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the
* life cycle of the service. The service will call {@link DownloadManager#startDownloads()} and * life cycle of the process.
* {@link DownloadManager#stopDownloads} as necessary when requirements returned by {@link
* #getRequirements()} are met or stop being met.
*/ */
protected abstract DownloadManager getDownloadManager(); protected abstract DownloadManager getDownloadManager();
@ -324,14 +311,6 @@ public abstract class DownloadService extends Service {
*/ */
protected abstract @Nullable Scheduler getScheduler(); protected abstract @Nullable Scheduler getScheduler();
/**
* Returns requirements for downloads to take place. By default the only requirement is that the
* device has network connectivity.
*/
protected Requirements getRequirements() {
return DEFAULT_REQUIREMENTS;
}
/** /**
* Should be overridden in the subclass if the service will be run in the foreground. * Should be overridden in the subclass if the service will be run in the foreground.
* *
@ -363,32 +342,16 @@ public abstract class DownloadService extends Service {
// Do nothing. // Do nothing.
} }
private void maybeStartWatchingRequirements(Requirements requirements) { private void notifyDownloadStateChange(DownloadState downloadState) {
if (downloadManager.getDownloadCount() == 0) { onDownloadStateChanged(downloadState);
return; if (foregroundNotificationUpdater != null) {
if (downloadState.state == DownloadState.STATE_DOWNLOADING
|| downloadState.state == DownloadState.STATE_REMOVING
|| downloadState.state == DownloadState.STATE_RESTARTING) {
foregroundNotificationUpdater.startPeriodicUpdates();
} else {
foregroundNotificationUpdater.update();
} }
Class<? extends DownloadService> clazz = getClass();
RequirementsHelper requirementsHelper = requirementsHelpers.get(clazz);
if (requirementsHelper == null) {
requirementsHelper = new RequirementsHelper(this, requirements, getScheduler(), clazz);
requirementsHelpers.put(clazz, requirementsHelper);
requirementsHelper.start();
logd("started watching requirements");
}
}
private void maybeStopWatchingRequirements() {
if (downloadManager.getDownloadCount() > 0) {
return;
}
stopWatchingRequirements();
}
private void stopWatchingRequirements() {
RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass());
if (requirementsHelper != null) {
requirementsHelper.stop();
logd("stopped watching requirements");
} }
} }
@ -420,33 +383,6 @@ public abstract class DownloadService extends Service {
return new Intent(context, clazz).setAction(action); return new Intent(context, clazz).setAction(action);
} }
private final class DownloadManagerListener implements DownloadManager.Listener {
@Override
public void onInitialized(DownloadManager downloadManager) {
maybeStartWatchingRequirements(getRequirements());
}
@Override
public void onDownloadStateChanged(
DownloadManager downloadManager, DownloadState downloadState) {
DownloadService.this.onDownloadStateChanged(downloadState);
if (foregroundNotificationUpdater != null) {
if (downloadState.state == DownloadState.STATE_DOWNLOADING
|| downloadState.state == DownloadState.STATE_REMOVING
|| downloadState.state == DownloadState.STATE_RESTARTING) {
foregroundNotificationUpdater.startPeriodicUpdates();
} else {
foregroundNotificationUpdater.update();
}
}
}
@Override
public final void onIdle(DownloadManager downloadManager) {
stop();
}
}
private final class ForegroundNotificationUpdater implements Runnable { private final class ForegroundNotificationUpdater implements Runnable {
private final int notificationId; private final int notificationId;
@ -494,58 +430,87 @@ public abstract class DownloadService extends Service {
} }
} }
private static final class RequirementsHelper implements RequirementsWatcher.Listener { private static final class DownloadManagerHelper implements DownloadManager.Listener {
private final Context context; private final Context context;
private final Requirements requirements; private final DownloadManager downloadManager;
private final @Nullable Scheduler scheduler; @Nullable private final Scheduler scheduler;
private final Class<? extends DownloadService> serviceClass; private final Class<? extends DownloadService> serviceClass;
private final RequirementsWatcher requirementsWatcher; @Nullable private DownloadService downloadService;
private RequirementsHelper( private DownloadManagerHelper(
Context context, Context context,
Requirements requirements, DownloadManager downloadManager,
@Nullable Scheduler scheduler, @Nullable Scheduler scheduler,
Class<? extends DownloadService> serviceClass) { Class<? extends DownloadService> serviceClass) {
this.context = context; this.context = context;
this.requirements = requirements; this.downloadManager = downloadManager;
this.scheduler = scheduler; this.scheduler = scheduler;
this.serviceClass = serviceClass; this.serviceClass = serviceClass;
requirementsWatcher = new RequirementsWatcher(context, this, requirements); downloadManager.addListener(this);
}
public void start() {
requirementsWatcher.start();
}
public void stop() {
requirementsWatcher.stop();
if (scheduler != null) { if (scheduler != null) {
Requirements requirements = downloadManager.getRequirements();
setSchedulerEnabled(/* enabled= */ !requirements.checkRequirements(context), requirements);
}
}
public void attachService(DownloadService downloadService) {
Assertions.checkState(this.downloadService == null);
this.downloadService = downloadService;
}
public void detachService(DownloadService downloadService, boolean unschedule) {
Assertions.checkState(this.downloadService == downloadService);
this.downloadService = null;
if (scheduler != null && unschedule) {
scheduler.cancel(); scheduler.cancel();
} }
} }
@Override @Override
public void requirementsMet(RequirementsWatcher requirementsWatcher) { public void onInitialized(DownloadManager downloadManager) {
// Do nothing.
}
@Override
public void onDownloadStateChanged(
DownloadManager downloadManager, DownloadState downloadState) {
if (downloadService != null) {
downloadService.notifyDownloadStateChange(downloadState);
}
}
@Override
public final void onIdle(DownloadManager downloadManager) {
if (downloadService != null) {
downloadService.stop();
}
}
@Override
public void onRequirementsStateChanged(
DownloadManager downloadManager,
Requirements requirements,
@Requirements.RequirementFlags int notMetRequirements) {
boolean requirementsMet = notMetRequirements == 0;
if (downloadService == null && requirementsMet) {
try { try {
notifyService(); Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);
} catch (Exception e) { context.startService(intent);
/* If we can't notify the service, don't stop the scheduler. */ } catch (IllegalStateException e) {
/* startService fails if the app is in the background then don't stop the scheduler. */
return; return;
} }
}
if (scheduler != null) { if (scheduler != null) {
scheduler.cancel(); setSchedulerEnabled(/* enabled= */ !requirementsMet, requirements);
} }
} }
@Override private void setSchedulerEnabled(boolean enabled, Requirements requirements) {
public void requirementsNotMet(RequirementsWatcher requirementsWatcher) { if (!enabled) {
try { scheduler.cancel();
notifyService(); } else {
} catch (Exception e) {
/* Do nothing. The service isn't running anyway. */
}
if (scheduler != null) {
String servicePackage = context.getPackageName(); String servicePackage = context.getPackageName();
boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART); boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART);
if (!success) { if (!success) {
@ -553,15 +518,5 @@ public abstract class DownloadService extends Service {
} }
} }
} }
private void notifyService() throws Exception {
Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);
try {
context.startService(intent);
} catch (IllegalStateException e) {
/* startService will fail if the app is in the background and the service isn't running. */
throw new Exception(e);
}
}
} }
} }

View File

@ -13,17 +13,20 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.offline; package com.google.android.exoplayer2.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.Requirements.RequirementFlags;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.HashSet;
/** Represents state of a download. */ /** Represents state of a download. */
public final class DownloadState { public final class DownloadState {
@ -74,19 +77,19 @@ public final class DownloadState {
public static final int FAILURE_REASON_UNKNOWN = 1; public static final int FAILURE_REASON_UNKNOWN = 1;
/** /**
* Download stop flags. Possible flag values are {@link #STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY} and * Download stop flags. Possible flag values are {@link #STOP_FLAG_MANUAL} and {@link
* {@link #STOP_FLAG_STOPPED}. * #STOP_FLAG_REQUIREMENTS_NOT_MET}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef( @IntDef(
flag = true, flag = true,
value = {STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY, STOP_FLAG_STOPPED}) value = {STOP_FLAG_MANUAL, STOP_FLAG_REQUIREMENTS_NOT_MET})
public @interface StopFlags {} public @interface StopFlags {}
/** Download can't be started as the manager isn't ready. */ /** Download is stopped by the application. */
public static final int STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY = 1; public static final int STOP_FLAG_MANUAL = 1;
/** All downloads are stopped by the application. */ /** Download is stopped as the requirements are not met. */
public static final int STOP_FLAG_STOPPED = 1 << 1; public static final int STOP_FLAG_REQUIREMENTS_NOT_MET = 1 << 1;
/** Returns the state string for the given state value. */ /** Returns the state string for the given state value. */
public static String getStateString(@State int state) { public static String getStateString(@State int state) {
@ -154,7 +157,40 @@ public final class DownloadState {
*/ */
@FailureReason public final int failureReason; @FailureReason public final int failureReason;
/** Download stop flags. These flags stop downloading any content. */ /** Download stop flags. These flags stop downloading any content. */
public final int stopFlags; @StopFlags public final int stopFlags;
/** Not met requirements to download. */
@Requirements.RequirementFlags public final int notMetRequirements;
/** If {@link #STOP_FLAG_MANUAL} is set then this field holds the manual stop reason. */
public final int manualStopReason;
/**
* Creates a {@link DownloadState} using a {@link DownloadAction}.
*
* @param action The {@link DownloadAction}.
*/
public DownloadState(DownloadAction action) {
this(action, System.currentTimeMillis());
}
private DownloadState(DownloadAction action, long currentTimeMs) {
this(
action.id,
action.type,
action.uri,
action.customCacheKey,
/* state= */ action.isRemoveAction ? STATE_REMOVING : STATE_QUEUED,
/* downloadPercentage= */ C.PERCENTAGE_UNSET,
/* downloadedBytes= */ 0,
/* totalBytes= */ C.LENGTH_UNSET,
FAILURE_REASON_NONE,
/* stopFlags= */ 0,
/* notMetRequirements= */ 0,
/* manualStopReason= */ 0,
/* startTimeMs= */ currentTimeMs,
/* updateTimeMs= */ currentTimeMs,
action.keys.toArray(new StreamKey[0]),
action.data);
}
/* package */ DownloadState( /* package */ DownloadState(
String id, String id,
@ -167,28 +203,91 @@ public final class DownloadState {
long totalBytes, long totalBytes,
@FailureReason int failureReason, @FailureReason int failureReason,
@StopFlags int stopFlags, @StopFlags int stopFlags,
@RequirementFlags int notMetRequirements,
int manualStopReason,
long startTimeMs, long startTimeMs,
long updateTimeMs, long updateTimeMs,
StreamKey[] streamKeys, StreamKey[] streamKeys,
byte[] customMetadata) { byte[] customMetadata) {
this.stopFlags = stopFlags; Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED));
Assertions.checkState(stopFlags == 0 || (state != STATE_DOWNLOADING && state != STATE_QUEUED));
Assertions.checkState( Assertions.checkState(
failureReason == FAILURE_REASON_NONE ? state != STATE_FAILED : state == STATE_FAILED); ((stopFlags & STOP_FLAG_REQUIREMENTS_NOT_MET) == 0) == (notMetRequirements == 0));
// TODO enable this when we start changing state immediately Assertions.checkState(((stopFlags & STOP_FLAG_MANUAL) != 0) || (manualStopReason == 0));
// Assertions.checkState(stopFlags == 0 || (state != STATE_DOWNLOADING && state !=
// STATE_QUEUED));
this.id = id; this.id = id;
this.type = type; this.type = type;
this.uri = uri; this.uri = uri;
this.cacheKey = cacheKey; this.cacheKey = cacheKey;
this.streamKeys = streamKeys;
this.customMetadata = customMetadata;
this.state = state; this.state = state;
this.downloadPercentage = downloadPercentage; this.downloadPercentage = downloadPercentage;
this.downloadedBytes = downloadedBytes; this.downloadedBytes = downloadedBytes;
this.totalBytes = totalBytes; this.totalBytes = totalBytes;
this.failureReason = failureReason; this.failureReason = failureReason;
this.stopFlags = stopFlags;
this.notMetRequirements = notMetRequirements;
this.manualStopReason = manualStopReason;
this.startTimeMs = startTimeMs; this.startTimeMs = startTimeMs;
this.updateTimeMs = updateTimeMs; this.updateTimeMs = updateTimeMs;
this.streamKeys = streamKeys;
this.customMetadata = customMetadata;
}
/**
* Merges the given {@link DownloadAction} and creates a new {@link DownloadState}. The action
* must have the same id and type.
*
* @param action The {@link DownloadAction} to be merged.
* @return A new {@link DownloadState}.
*/
public DownloadState mergeAction(DownloadAction action) {
Assertions.checkArgument(action.id.equals(id));
Assertions.checkArgument(action.type.equals(type));
return new DownloadState(
id,
type,
action.uri,
action.customCacheKey,
getNextState(action, state),
/* downloadPercentage= */ C.PERCENTAGE_UNSET,
downloadedBytes,
/* totalBytes= */ C.LENGTH_UNSET,
FAILURE_REASON_NONE,
stopFlags,
notMetRequirements,
manualStopReason,
startTimeMs,
updateTimeMs,
mergeStreamKeys(this, action),
action.data);
}
private static int getNextState(DownloadAction action, int currentState) {
int newState;
if (action.isRemoveAction) {
newState = STATE_REMOVING;
} else {
if (currentState == STATE_REMOVING || currentState == STATE_RESTARTING) {
newState = STATE_RESTARTING;
} else if (currentState == STATE_STOPPED) {
newState = STATE_STOPPED;
} else {
newState = STATE_QUEUED;
}
}
return newState;
}
private static StreamKey[] mergeStreamKeys(DownloadState downloadState, DownloadAction action) {
StreamKey[] streamKeys = downloadState.streamKeys;
if (!action.isRemoveAction && streamKeys.length > 0) {
if (action.keys.isEmpty()) {
streamKeys = new StreamKey[0];
} else {
HashSet<StreamKey> keys = new HashSet<>(action.keys);
Collections.addAll(keys, downloadState.streamKeys);
streamKeys = keys.toArray(new StreamKey[0]);
}
}
return streamKeys;
} }
} }

View File

@ -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 &lt;= position &lt;=
* 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();
}

View File

@ -109,16 +109,16 @@ public final class DownloaderConstructorHelper {
cacheReadDataSourceFactory != null cacheReadDataSourceFactory != null
? cacheReadDataSourceFactory ? cacheReadDataSourceFactory
: new FileDataSourceFactory(); : new FileDataSourceFactory();
DataSink.Factory writeDataSinkFactory = if (cacheWriteDataSinkFactory == null) {
cacheWriteDataSinkFactory != null cacheWriteDataSinkFactory =
? cacheWriteDataSinkFactory new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE);
: new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_MAX_CACHE_FILE_SIZE); }
onlineCacheDataSourceFactory = onlineCacheDataSourceFactory =
new CacheDataSourceFactory( new CacheDataSourceFactory(
cache, cache,
upstreamFactory, upstreamFactory,
readDataSourceFactory, readDataSourceFactory,
writeDataSinkFactory, cacheWriteDataSinkFactory,
CacheDataSource.FLAG_BLOCK_ON_CACHE, CacheDataSource.FLAG_BLOCK_ON_CACHE,
/* eventListener= */ null, /* eventListener= */ null,
cacheKeyFactory); cacheKeyFactory);

View File

@ -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);
}
}

View File

@ -53,7 +53,11 @@ public final class ProgressiveDownloader implements Downloader {
Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) { Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) {
this.dataSpec = this.dataSpec =
new DataSpec( new DataSpec(
uri, /* absoluteStreamPosition= */ 0, C.LENGTH_UNSET, customCacheKey, /* flags= */ 0); uri,
/* absoluteStreamPosition= */ 0,
C.LENGTH_UNSET,
customCacheKey,
/* flags= */ DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);
this.cache = constructorHelper.getCache(); this.cache = constructorHelper.getCache();
this.dataSource = constructorHelper.createCacheDataSource(); this.dataSource = constructorHelper.createCacheDataSource();
this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();

View File

@ -155,16 +155,14 @@ public final class Requirements {
* @return Whether the requirements are met. * @return Whether the requirements are met.
*/ */
public boolean checkRequirements(Context context) { public boolean checkRequirements(Context context) {
return checkNetworkRequirements(context) return getNotMetRequirements(context) == 0;
&& checkChargingRequirement(context)
&& checkIdleRequirement(context);
} }
/** /**
* Returns the requirement flags that are not met, or 0. * Returns {@link RequirementFlags} that are not met, or 0.
* *
* @param context Any context. * @param context Any context.
* @return The requirement flags that are not met, or 0. * @return RequirementFlags that are not met, or 0.
*/ */
@RequirementFlags @RequirementFlags
public int getNotMetRequirements(Context context) { public int getNotMetRequirements(Context context) {
@ -202,7 +200,7 @@ public final class Requirements {
logd("Roaming: " + roaming); logd("Roaming: " + roaming);
return !roaming; return !roaming;
} }
boolean activeNetworkMetered = isActiveNetworkMetered(connectivityManager, networkInfo); boolean activeNetworkMetered = connectivityManager.isActiveNetworkMetered();
logd("Metered network: " + activeNetworkMetered); logd("Metered network: " + activeNetworkMetered);
if (networkRequirement == NETWORK_TYPE_UNMETERED) { if (networkRequirement == NETWORK_TYPE_UNMETERED) {
return !activeNetworkMetered; return !activeNetworkMetered;
@ -257,17 +255,6 @@ public final class Requirements {
return !validated; return !validated;
} }
private static boolean isActiveNetworkMetered(
ConnectivityManager connectivityManager, NetworkInfo networkInfo) {
if (Util.SDK_INT >= 16) {
return connectivityManager.isActiveNetworkMetered();
}
int type = networkInfo.getType();
return type != ConnectivityManager.TYPE_WIFI
&& type != ConnectivityManager.TYPE_BLUETOOTH
&& type != ConnectivityManager.TYPE_ETHERNET;
}
private static void logd(String message) { private static void logd(String message) {
if (Scheduler.DEBUG) { if (Scheduler.DEBUG) {
Log.d(TAG, message); Log.d(TAG, message);
@ -285,4 +272,20 @@ public final class Requirements {
+ (isIdleRequired() ? ",idle" : "") + (isIdleRequired() ? ",idle" : "")
+ '}'; + '}';
} }
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
return requirements == ((Requirements) o).requirements;
}
@Override
public int hashCode() {
return requirements;
}
} }

View File

@ -42,21 +42,16 @@ public final class RequirementsWatcher {
* Requirements} are met. * Requirements} are met.
*/ */
public interface Listener { public interface Listener {
/** /**
* Called when all of the requirements are met. * Called when there is a change on the met requirements.
* *
* @param requirementsWatcher Calling instance. * @param requirementsWatcher Calling instance.
* @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not
* met, or 0.
*/ */
void requirementsMet(RequirementsWatcher requirementsWatcher); void onRequirementsStateChanged(
RequirementsWatcher requirementsWatcher,
/** @Requirements.RequirementFlags int notMetRequirements);
* Called when there is at least one not met requirement and there is a change on which of the
* requirements are not met.
*
* @param requirementsWatcher Calling instance.
*/
void requirementsNotMet(RequirementsWatcher requirementsWatcher);
} }
private static final String TAG = "RequirementsWatcher"; private static final String TAG = "RequirementsWatcher";
@ -66,8 +61,9 @@ public final class RequirementsWatcher {
private final Requirements requirements; private final Requirements requirements;
private DeviceStatusChangeReceiver receiver; private DeviceStatusChangeReceiver receiver;
private int notMetRequirements; @Requirements.RequirementFlags private int notMetRequirements;
private CapabilityValidatedCallback networkCallback; private CapabilityValidatedCallback networkCallback;
private Handler handler;
/** /**
* @param context Any context. * @param context Any context.
@ -84,9 +80,13 @@ public final class RequirementsWatcher {
/** /**
* Starts watching for changes. Must be called from a thread that has an associated {@link * Starts watching for changes. Must be called from a thread that has an associated {@link
* Looper}. Listener methods are called on the caller thread. * Looper}. Listener methods are called on the caller thread.
*
* @return Initial {@link Requirements.RequirementFlags RequirementFlags} that are not met, or 0.
*/ */
public void start() { @Requirements.RequirementFlags
public int start() {
Assertions.checkNotNull(Looper.myLooper()); Assertions.checkNotNull(Looper.myLooper());
handler = new Handler();
notMetRequirements = requirements.getNotMetRequirements(context); notMetRequirements = requirements.getNotMetRequirements(context);
@ -111,8 +111,9 @@ public final class RequirementsWatcher {
} }
} }
receiver = new DeviceStatusChangeReceiver(); receiver = new DeviceStatusChangeReceiver();
context.registerReceiver(receiver, filter, null, new Handler()); context.registerReceiver(receiver, filter, null, handler);
logd(this + " started"); logd(this + " started");
return notMetRequirements;
} }
/** Stops watching for changes. */ /** Stops watching for changes. */
@ -160,18 +161,12 @@ public final class RequirementsWatcher {
} }
private void checkRequirements() { private void checkRequirements() {
@Requirements.RequirementFlags
int notMetRequirements = requirements.getNotMetRequirements(context); int notMetRequirements = requirements.getNotMetRequirements(context);
if (this.notMetRequirements == notMetRequirements) { if (this.notMetRequirements != notMetRequirements) {
logd("notMetRequirements hasn't changed: " + notMetRequirements);
return;
}
this.notMetRequirements = notMetRequirements; this.notMetRequirements = notMetRequirements;
if (notMetRequirements == 0) { logd("notMetRequirements has changed: " + notMetRequirements);
logd("start job"); listener.onRequirementsStateChanged(this, notMetRequirements);
listener.requirementsMet(this);
} else {
logd("stop job");
listener.requirementsNotMet(this);
} }
} }
@ -195,16 +190,22 @@ public final class RequirementsWatcher {
private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback { private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback {
@Override @Override
public void onAvailable(Network network) { public void onAvailable(Network network) {
super.onAvailable(network); onNetworkCallback();
logd(RequirementsWatcher.this + " NetworkCallback.onAvailable");
checkRequirements();
} }
@Override @Override
public void onLost(Network network) { public void onLost(Network network) {
super.onLost(network); onNetworkCallback();
logd(RequirementsWatcher.this + " NetworkCallback.onLost"); }
private void onNetworkCallback() {
handler.post(
() -> {
if (networkCallback != null) {
logd(RequirementsWatcher.this + " NetworkCallback");
checkRequirements(); checkRequirements();
} }
});
}
} }
} }

View File

@ -206,10 +206,10 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> {
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
ClippingMediaPeriod mediaPeriod = ClippingMediaPeriod mediaPeriod =
new ClippingMediaPeriod( new ClippingMediaPeriod(
mediaSource.createPeriod(id, allocator), mediaSource.createPeriod(id, allocator, startPositionUs),
enableInitialDiscontinuity, enableInitialDiscontinuity,
periodStartUs, periodStartUs,
periodEndUs); periodEndUs);

View File

@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.EventDispatcher;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -36,9 +35,11 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
@ -51,12 +52,19 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
private static final int MSG_REMOVE = 1; private static final int MSG_REMOVE = 1;
private static final int MSG_MOVE = 2; private static final int MSG_MOVE = 2;
private static final int MSG_SET_SHUFFLE_ORDER = 3; private static final int MSG_SET_SHUFFLE_ORDER = 3;
private static final int MSG_NOTIFY_LISTENER = 4; private static final int MSG_UPDATE_TIMELINE = 4;
private static final int MSG_ON_COMPLETION = 5; private static final int MSG_ON_COMPLETION = 5;
// Accessed on any thread. // Accessed on any thread.
@GuardedBy("this")
private final List<MediaSourceHolder> mediaSourcesPublic; private final List<MediaSourceHolder> mediaSourcesPublic;
@Nullable private Handler playbackThreadHandler;
@GuardedBy("this")
private final Set<HandlerAndRunnable> pendingOnCompletionActions;
@GuardedBy("this")
@Nullable
private Handler playbackThreadHandler;
// Accessed on the playback thread only. // Accessed on the playback thread only.
private final List<MediaSourceHolder> mediaSourceHolders; private final List<MediaSourceHolder> mediaSourceHolders;
@ -67,8 +75,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
private final Timeline.Window window; private final Timeline.Window window;
private final Timeline.Period period; private final Timeline.Period period;
private boolean listenerNotificationScheduled; private boolean timelineUpdateScheduled;
private EventDispatcher<Runnable> pendingOnCompletionActions; private Set<HandlerAndRunnable> nextTimelineUpdateOnCompletionActions;
private ShuffleOrder shuffleOrder; private ShuffleOrder shuffleOrder;
private int windowCount; private int windowCount;
private int periodCount; private int periodCount;
@ -127,7 +135,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
this.mediaSourceByUid = new HashMap<>(); this.mediaSourceByUid = new HashMap<>();
this.mediaSourcesPublic = new ArrayList<>(); this.mediaSourcesPublic = new ArrayList<>();
this.mediaSourceHolders = new ArrayList<>(); this.mediaSourceHolders = new ArrayList<>();
this.pendingOnCompletionActions = new EventDispatcher<>(); this.nextTimelineUpdateOnCompletionActions = new HashSet<>();
this.pendingOnCompletionActions = new HashSet<>();
this.isAtomic = isAtomic; this.isAtomic = isAtomic;
this.useLazyPreparation = useLazyPreparation; this.useLazyPreparation = useLazyPreparation;
window = new Timeline.Window(); window = new Timeline.Window();
@ -148,13 +157,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* Appends a {@link MediaSource} to the playlist and executes a custom action on completion. * Appends a {@link MediaSource} to the playlist and executes a custom action on completion.
* *
* @param mediaSource The {@link MediaSource} to be added to the list. * @param mediaSource The {@link MediaSource} to be added to the list.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* source has been added to the playlist. * source has been added to the playlist.
*/ */
public final synchronized void addMediaSource( public final synchronized void addMediaSource(
MediaSource mediaSource, Handler handler, Runnable actionOnCompletion) { MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, actionOnCompletion); addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, onCompletionAction);
} }
/** /**
@ -169,7 +178,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
index, index,
Collections.singletonList(mediaSource), Collections.singletonList(mediaSource),
/* handler= */ null, /* handler= */ null,
/* actionOnCompletion= */ null); /* onCompletionAction= */ null);
} }
/** /**
@ -178,14 +187,14 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* @param index The index at which the new {@link MediaSource} will be inserted. This index must * @param index The index at which the new {@link MediaSource} will be inserted. This index must
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}. * be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSource The {@link MediaSource} to be added to the list. * @param mediaSource The {@link MediaSource} to be added to the list.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* source has been added to the playlist. * source has been added to the playlist.
*/ */
public final synchronized void addMediaSource( public final synchronized void addMediaSource(
int index, MediaSource mediaSource, Handler handler, Runnable actionOnCompletion) { int index, MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
addPublicMediaSources( addPublicMediaSources(
index, Collections.singletonList(mediaSource), handler, actionOnCompletion); index, Collections.singletonList(mediaSource), handler, onCompletionAction);
} }
/** /**
@ -199,7 +208,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
mediaSourcesPublic.size(), mediaSourcesPublic.size(),
mediaSources, mediaSources,
/* handler= */ null, /* handler= */ null,
/* actionOnCompletion= */ null); /* onCompletionAction= */ null);
} }
/** /**
@ -208,13 +217,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* *
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection. * sources are added in the order in which they appear in this collection.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* sources have been added to the playlist. * sources have been added to the playlist.
*/ */
public final synchronized void addMediaSources( public final synchronized void addMediaSources(
Collection<MediaSource> mediaSources, Handler handler, Runnable actionOnCompletion) { Collection<MediaSource> mediaSources, Handler handler, Runnable onCompletionAction) {
addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, actionOnCompletion); addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction);
} }
/** /**
@ -226,7 +235,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* sources are added in the order in which they appear in this collection. * sources are added in the order in which they appear in this collection.
*/ */
public final synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) { public final synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) {
addPublicMediaSources(index, mediaSources, /* handler= */ null, /* actionOnCompletion= */ null); addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null);
} }
/** /**
@ -236,16 +245,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}. * be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection. * sources are added in the order in which they appear in this collection.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* sources have been added to the playlist. * sources have been added to the playlist.
*/ */
public final synchronized void addMediaSources( public final synchronized void addMediaSources(
int index, int index,
Collection<MediaSource> mediaSources, Collection<MediaSource> mediaSources,
Handler handler, Handler handler,
Runnable actionOnCompletion) { Runnable onCompletionAction) {
addPublicMediaSources(index, mediaSources, handler, actionOnCompletion); addPublicMediaSources(index, mediaSources, handler, onCompletionAction);
} }
/** /**
@ -261,7 +270,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* range of 0 &lt;= index &lt; {@link #getSize()}. * range of 0 &lt;= index &lt; {@link #getSize()}.
*/ */
public final synchronized void removeMediaSource(int index) { public final synchronized void removeMediaSource(int index) {
removePublicMediaSources(index, index + 1, /* handler= */ null, /* actionOnCompletion= */ null); removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null);
} }
/** /**
@ -275,13 +284,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* *
* @param index The index at which the media source will be removed. This index must be in the * @param index The index at which the media source will be removed. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}. * range of 0 &lt;= index &lt; {@link #getSize()}.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* source has been removed from the playlist. * source has been removed from the playlist.
*/ */
public final synchronized void removeMediaSource( public final synchronized void removeMediaSource(
int index, Handler handler, Runnable actionOnCompletion) { int index, Handler handler, Runnable onCompletionAction) {
removePublicMediaSources(index, index + 1, handler, actionOnCompletion); removePublicMediaSources(index, index + 1, handler, onCompletionAction);
} }
/** /**
@ -300,7 +309,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
*/ */
public final synchronized void removeMediaSourceRange(int fromIndex, int toIndex) { public final synchronized void removeMediaSourceRange(int fromIndex, int toIndex) {
removePublicMediaSources( removePublicMediaSources(
fromIndex, toIndex, /* handler= */ null, /* actionOnCompletion= */ null); fromIndex, toIndex, /* handler= */ null, /* onCompletionAction= */ null);
} }
/** /**
@ -314,15 +323,15 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}. * removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param toIndex The final range index, pointing to the first media source that will be left * @param toIndex The final range index, pointing to the first media source that will be left
* untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}. * untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* source range has been removed from the playlist. * source range has been removed from the playlist.
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0, * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
* {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex} * {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex}
*/ */
public final synchronized void removeMediaSourceRange( public final synchronized void removeMediaSourceRange(
int fromIndex, int toIndex, Handler handler, Runnable actionOnCompletion) { int fromIndex, int toIndex, Handler handler, Runnable onCompletionAction) {
removePublicMediaSources(fromIndex, toIndex, handler, actionOnCompletion); removePublicMediaSources(fromIndex, toIndex, handler, onCompletionAction);
} }
/** /**
@ -335,7 +344,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
*/ */
public final synchronized void moveMediaSource(int currentIndex, int newIndex) { public final synchronized void moveMediaSource(int currentIndex, int newIndex) {
movePublicMediaSource( movePublicMediaSource(
currentIndex, newIndex, /* handler= */ null, /* actionOnCompletion= */ null); currentIndex, newIndex, /* handler= */ null, /* onCompletionAction= */ null);
} }
/** /**
@ -346,13 +355,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* in the range of 0 &lt;= index &lt; {@link #getSize()}. * in the range of 0 &lt;= index &lt; {@link #getSize()}.
* @param newIndex The target index of the media source in the playlist. This index must be in the * @param newIndex The target index of the media source in the playlist. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}. * range of 0 &lt;= index &lt; {@link #getSize()}.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* source has been moved. * source has been moved.
*/ */
public final synchronized void moveMediaSource( public final synchronized void moveMediaSource(
int currentIndex, int newIndex, Handler handler, Runnable actionOnCompletion) { int currentIndex, int newIndex, Handler handler, Runnable onCompletionAction) {
movePublicMediaSource(currentIndex, newIndex, handler, actionOnCompletion); movePublicMediaSource(currentIndex, newIndex, handler, onCompletionAction);
} }
/** Clears the playlist. */ /** Clears the playlist. */
@ -363,12 +372,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
/** /**
* Clears the playlist and executes a custom action on completion. * Clears the playlist and executes a custom action on completion.
* *
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the playlist * @param onCompletionAction A {@link Runnable} which is executed immediately after the playlist
* has been cleared. * has been cleared.
*/ */
public final synchronized void clear(Handler handler, Runnable actionOnCompletion) { public final synchronized void clear(Handler handler, Runnable onCompletionAction) {
removeMediaSourceRange(0, getSize(), handler, actionOnCompletion); removeMediaSourceRange(0, getSize(), handler, onCompletionAction);
} }
/** Returns the number of media sources in the playlist. */ /** Returns the number of media sources in the playlist. */
@ -392,20 +401,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* @param shuffleOrder A {@link ShuffleOrder}. * @param shuffleOrder A {@link ShuffleOrder}.
*/ */
public final synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) { public final synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) {
setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* actionOnCompletion= */ null); setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* onCompletionAction= */ null);
} }
/** /**
* Sets a new shuffle order to use when shuffling the child media sources. * Sets a new shuffle order to use when shuffling the child media sources.
* *
* @param shuffleOrder A {@link ShuffleOrder}. * @param shuffleOrder A {@link ShuffleOrder}.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the shuffle * @param onCompletionAction A {@link Runnable} which is executed immediately after the shuffle
* order has been changed. * order has been changed.
*/ */
public final synchronized void setShuffleOrder( public final synchronized void setShuffleOrder(
ShuffleOrder shuffleOrder, Handler handler, Runnable actionOnCompletion) { ShuffleOrder shuffleOrder, Handler handler, Runnable onCompletionAction) {
setPublicShuffleOrder(shuffleOrder, handler, actionOnCompletion); setPublicShuffleOrder(shuffleOrder, handler, onCompletionAction);
} }
// CompositeMediaSource implementation. // CompositeMediaSource implementation.
@ -422,11 +431,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
super.prepareSourceInternal(mediaTransferListener); super.prepareSourceInternal(mediaTransferListener);
playbackThreadHandler = new Handler(/* callback= */ this::handleMessage); playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);
if (mediaSourcesPublic.isEmpty()) { if (mediaSourcesPublic.isEmpty()) {
notifyListener(); updateTimelineAndScheduleOnCompletionActions();
} else { } else {
shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());
addMediaSourcesInternal(0, mediaSourcesPublic); addMediaSourcesInternal(0, mediaSourcesPublic);
scheduleListenerNotification(); scheduleTimelineUpdate();
} }
} }
@ -438,7 +447,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
} }
@Override @Override
public final MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public final MediaPeriod createPeriod(
MediaPeriodId id, Allocator allocator, long startPositionUs) {
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid); MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid);
if (holder == null) { if (holder == null) {
@ -446,7 +456,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
holder = new MediaSourceHolder(new DummyMediaSource()); holder = new MediaSourceHolder(new DummyMediaSource());
holder.hasStartedPreparing = true; holder.hasStartedPreparing = true;
} }
DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, id, allocator); DeferredMediaPeriod mediaPeriod =
new DeferredMediaPeriod(holder.mediaSource, id, allocator, startPositionUs);
mediaSourceByMediaPeriod.put(mediaPeriod, holder); mediaSourceByMediaPeriod.put(mediaPeriod, holder);
holder.activeMediaPeriods.add(mediaPeriod); holder.activeMediaPeriods.add(mediaPeriod);
if (!holder.hasStartedPreparing) { if (!holder.hasStartedPreparing) {
@ -473,10 +484,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
super.releaseSourceInternal(); super.releaseSourceInternal();
mediaSourceHolders.clear(); mediaSourceHolders.clear();
mediaSourceByUid.clear(); mediaSourceByUid.clear();
playbackThreadHandler = null;
shuffleOrder = shuffleOrder.cloneAndClear(); shuffleOrder = shuffleOrder.cloneAndClear();
windowCount = 0; windowCount = 0;
periodCount = 0; periodCount = 0;
if (playbackThreadHandler != null) {
playbackThreadHandler.removeCallbacksAndMessages(null);
playbackThreadHandler = null;
}
timelineUpdateScheduled = false;
nextTimelineUpdateOnCompletionActions.clear();
dispatchOnCompletionActions(pendingOnCompletionActions);
} }
@Override @Override
@ -516,8 +533,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
int index, int index,
Collection<MediaSource> mediaSources, Collection<MediaSource> mediaSources,
@Nullable Handler handler, @Nullable Handler handler,
@Nullable Runnable actionOnCompletion) { @Nullable Runnable onCompletionAction) {
Assertions.checkArgument((handler == null) == (actionOnCompletion == null)); Assertions.checkArgument((handler == null) == (onCompletionAction == null));
Handler playbackThreadHandler = this.playbackThreadHandler;
for (MediaSource mediaSource : mediaSources) { for (MediaSource mediaSource : mediaSources) {
Assertions.checkNotNull(mediaSource); Assertions.checkNotNull(mediaSource);
} }
@ -527,12 +545,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
} }
mediaSourcesPublic.addAll(index, mediaSourceHolders); mediaSourcesPublic.addAll(index, mediaSourceHolders);
if (playbackThreadHandler != null && !mediaSources.isEmpty()) { if (playbackThreadHandler != null && !mediaSources.isEmpty()) {
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
playbackThreadHandler playbackThreadHandler
.obtainMessage( .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, callbackAction))
MSG_ADD, new MessageData<>(index, mediaSourceHolders, handler, actionOnCompletion))
.sendToTarget(); .sendToTarget();
} else if (actionOnCompletion != null && handler != null) { } else if (onCompletionAction != null && handler != null) {
handler.post(actionOnCompletion); handler.post(onCompletionAction);
} }
} }
@ -541,16 +559,17 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
int fromIndex, int fromIndex,
int toIndex, int toIndex,
@Nullable Handler handler, @Nullable Handler handler,
@Nullable Runnable actionOnCompletion) { @Nullable Runnable onCompletionAction) {
Assertions.checkArgument((handler == null) == (actionOnCompletion == null)); Assertions.checkArgument((handler == null) == (onCompletionAction == null));
Handler playbackThreadHandler = this.playbackThreadHandler;
Util.removeRange(mediaSourcesPublic, fromIndex, toIndex); Util.removeRange(mediaSourcesPublic, fromIndex, toIndex);
if (playbackThreadHandler != null) { if (playbackThreadHandler != null) {
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
playbackThreadHandler playbackThreadHandler
.obtainMessage( .obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, callbackAction))
MSG_REMOVE, new MessageData<>(fromIndex, toIndex, handler, actionOnCompletion))
.sendToTarget(); .sendToTarget();
} else if (actionOnCompletion != null && handler != null) { } else if (onCompletionAction != null && handler != null) {
handler.post(actionOnCompletion); handler.post(onCompletionAction);
} }
} }
@ -559,23 +578,24 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
int currentIndex, int currentIndex,
int newIndex, int newIndex,
@Nullable Handler handler, @Nullable Handler handler,
@Nullable Runnable actionOnCompletion) { @Nullable Runnable onCompletionAction) {
Assertions.checkArgument((handler == null) == (actionOnCompletion == null)); Assertions.checkArgument((handler == null) == (onCompletionAction == null));
Handler playbackThreadHandler = this.playbackThreadHandler;
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
if (playbackThreadHandler != null) { if (playbackThreadHandler != null) {
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
playbackThreadHandler playbackThreadHandler
.obtainMessage( .obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, callbackAction))
MSG_MOVE, new MessageData<>(currentIndex, newIndex, handler, actionOnCompletion))
.sendToTarget(); .sendToTarget();
} else if (actionOnCompletion != null && handler != null) { } else if (onCompletionAction != null && handler != null) {
handler.post(actionOnCompletion); handler.post(onCompletionAction);
} }
} }
@GuardedBy("this") @GuardedBy("this")
private void setPublicShuffleOrder( private void setPublicShuffleOrder(
ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable actionOnCompletion) { ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable onCompletionAction) {
Assertions.checkArgument((handler == null) == (actionOnCompletion == null)); Assertions.checkArgument((handler == null) == (onCompletionAction == null));
Handler playbackThreadHandler = this.playbackThreadHandler; Handler playbackThreadHandler = this.playbackThreadHandler;
if (playbackThreadHandler != null) { if (playbackThreadHandler != null) {
int size = getSize(); int size = getSize();
@ -585,35 +605,44 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
.cloneAndClear() .cloneAndClear()
.cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size); .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
} }
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
playbackThreadHandler playbackThreadHandler
.obtainMessage( .obtainMessage(
MSG_SET_SHUFFLE_ORDER, MSG_SET_SHUFFLE_ORDER,
new MessageData<>(/* index= */ 0, shuffleOrder, handler, actionOnCompletion)) new MessageData<>(/* index= */ 0, shuffleOrder, callbackAction))
.sendToTarget(); .sendToTarget();
} else { } else {
this.shuffleOrder = this.shuffleOrder =
shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
if (actionOnCompletion != null && handler != null) { if (onCompletionAction != null && handler != null) {
handler.post(actionOnCompletion); handler.post(onCompletionAction);
} }
} }
} }
@GuardedBy("this")
@Nullable
private HandlerAndRunnable createOnCompletionAction(
@Nullable Handler handler, @Nullable Runnable runnable) {
if (handler == null || runnable == null) {
return null;
}
HandlerAndRunnable handlerAndRunnable = new HandlerAndRunnable(handler, runnable);
pendingOnCompletionActions.add(handlerAndRunnable);
return handlerAndRunnable;
}
// Internal methods. Called on the playback thread. // Internal methods. Called on the playback thread.
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private boolean handleMessage(Message msg) { private boolean handleMessage(Message msg) {
if (playbackThreadHandler == null) {
// Stale event.
return false;
}
switch (msg.what) { switch (msg.what) {
case MSG_ADD: case MSG_ADD:
MessageData<Collection<MediaSourceHolder>> addMessage = MessageData<Collection<MediaSourceHolder>> addMessage =
(MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj); (MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj);
shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size()); shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size());
addMediaSourcesInternal(addMessage.index, addMessage.customData); addMediaSourcesInternal(addMessage.index, addMessage.customData);
scheduleListenerNotification(addMessage.handler, addMessage.actionOnCompletion); scheduleTimelineUpdate(addMessage.onCompletionAction);
break; break;
case MSG_REMOVE: case MSG_REMOVE:
MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj); MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
@ -627,29 +656,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
for (int index = toIndex - 1; index >= fromIndex; index--) { for (int index = toIndex - 1; index >= fromIndex; index--) {
removeMediaSourceInternal(index); removeMediaSourceInternal(index);
} }
scheduleListenerNotification(removeMessage.handler, removeMessage.actionOnCompletion); scheduleTimelineUpdate(removeMessage.onCompletionAction);
break; break;
case MSG_MOVE: case MSG_MOVE:
MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj); MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1); shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1);
shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);
moveMediaSourceInternal(moveMessage.index, moveMessage.customData); moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
scheduleListenerNotification(moveMessage.handler, moveMessage.actionOnCompletion); scheduleTimelineUpdate(moveMessage.onCompletionAction);
break; break;
case MSG_SET_SHUFFLE_ORDER: case MSG_SET_SHUFFLE_ORDER:
MessageData<ShuffleOrder> shuffleOrderMessage = MessageData<ShuffleOrder> shuffleOrderMessage =
(MessageData<ShuffleOrder>) Util.castNonNull(msg.obj); (MessageData<ShuffleOrder>) Util.castNonNull(msg.obj);
shuffleOrder = shuffleOrderMessage.customData; shuffleOrder = shuffleOrderMessage.customData;
scheduleListenerNotification( scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction);
shuffleOrderMessage.handler, shuffleOrderMessage.actionOnCompletion);
break; break;
case MSG_NOTIFY_LISTENER: case MSG_UPDATE_TIMELINE:
notifyListener(); updateTimelineAndScheduleOnCompletionActions();
break; break;
case MSG_ON_COMPLETION: case MSG_ON_COMPLETION:
EventDispatcher<Runnable> actionsOnCompletion = Set<HandlerAndRunnable> actions = (Set<HandlerAndRunnable>) Util.castNonNull(msg.obj);
(EventDispatcher<Runnable>) Util.castNonNull(msg.obj); dispatchOnCompletionActions(actions);
actionsOnCompletion.dispatch(Runnable::run);
break; break;
default: default:
throw new IllegalStateException(); throw new IllegalStateException();
@ -657,36 +684,48 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
return true; return true;
} }
private void scheduleListenerNotification() { private void scheduleTimelineUpdate() {
scheduleListenerNotification(/* handler= */ null, /* actionOnCompletion= */ null); scheduleTimelineUpdate(/* onCompletionAction= */ null);
} }
private void scheduleListenerNotification( private void scheduleTimelineUpdate(@Nullable HandlerAndRunnable onCompletionAction) {
@Nullable Handler handler, @Nullable Runnable actionOnCompletion) { if (!timelineUpdateScheduled) {
if (!listenerNotificationScheduled) { getPlaybackThreadHandlerOnPlaybackThread().obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget();
Assertions.checkNotNull(playbackThreadHandler) timelineUpdateScheduled = true;
.obtainMessage(MSG_NOTIFY_LISTENER)
.sendToTarget();
listenerNotificationScheduled = true;
} }
if (actionOnCompletion != null && handler != null) { if (onCompletionAction != null) {
pendingOnCompletionActions.addListener(handler, actionOnCompletion); nextTimelineUpdateOnCompletionActions.add(onCompletionAction);
} }
} }
private void notifyListener() { private void updateTimelineAndScheduleOnCompletionActions() {
listenerNotificationScheduled = false; timelineUpdateScheduled = false;
EventDispatcher<Runnable> actionsOnCompletion = pendingOnCompletionActions; Set<HandlerAndRunnable> onCompletionActions = nextTimelineUpdateOnCompletionActions;
pendingOnCompletionActions = new EventDispatcher<>(); nextTimelineUpdateOnCompletionActions = new HashSet<>();
refreshSourceInfo( refreshSourceInfo(
new ConcatenatedTimeline( new ConcatenatedTimeline(
mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
/* manifest= */ null); /* manifest= */ null);
Assertions.checkNotNull(playbackThreadHandler) getPlaybackThreadHandlerOnPlaybackThread()
.obtainMessage(MSG_ON_COMPLETION, actionsOnCompletion) .obtainMessage(MSG_ON_COMPLETION, onCompletionActions)
.sendToTarget(); .sendToTarget();
} }
@SuppressWarnings("GuardedBy")
private Handler getPlaybackThreadHandlerOnPlaybackThread() {
// Write access to this value happens on the playback thread only, so playback thread reads
// don't need to be synchronized.
return Assertions.checkNotNull(playbackThreadHandler);
}
private synchronized void dispatchOnCompletionActions(
Set<HandlerAndRunnable> onCompletionActions) {
for (HandlerAndRunnable pendingAction : onCompletionActions) {
pendingAction.dispatch();
}
pendingOnCompletionActions.removeAll(onCompletionActions);
}
private void addMediaSourcesInternal( private void addMediaSourcesInternal(
int index, Collection<MediaSourceHolder> mediaSourceHolders) { int index, Collection<MediaSourceHolder> mediaSourceHolders) {
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
@ -761,6 +800,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
// unlikely to be a problem as a non-zero default position usually only occurs for live // unlikely to be a problem as a non-zero default position usually only occurs for live
// playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions // playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions
// anyway. // anyway.
timeline.getWindow(/* windowIndex= */ 0, window);
long windowStartPositionUs = window.getDefaultPositionUs(); long windowStartPositionUs = window.getDefaultPositionUs();
if (deferredMediaPeriod != null) { if (deferredMediaPeriod != null) {
long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs(); long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs();
@ -782,7 +822,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
} }
} }
mediaSourceHolder.isPrepared = true; mediaSourceHolder.isPrepared = true;
scheduleListenerNotification(); scheduleTimelineUpdate();
} }
private void removeMediaSourceInternal(int index) { private void removeMediaSourceInternal(int index) {
@ -895,15 +935,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public final int index; public final int index;
public final T customData; public final T customData;
@Nullable public final Handler handler; @Nullable public final HandlerAndRunnable onCompletionAction;
@Nullable public final Runnable actionOnCompletion;
public MessageData( public MessageData(int index, T customData, @Nullable HandlerAndRunnable onCompletionAction) {
int index, T customData, @Nullable Handler handler, @Nullable Runnable actionOnCompletion) {
this.index = index; this.index = index;
this.customData = customData; this.customData = customData;
this.handler = handler; this.onCompletionAction = onCompletionAction;
this.actionOnCompletion = actionOnCompletion;
} }
} }
@ -1144,7 +1181,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@ -1153,5 +1190,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
// Do nothing. // Do nothing.
} }
} }
private static final class HandlerAndRunnable {
private final Handler handler;
private final Runnable runnable;
public HandlerAndRunnable(Handler handler, Runnable runnable) {
this.handler = handler;
this.runnable = runnable;
}
public void dispatch() {
handler.post(runnable);
}
}
} }

View File

@ -25,7 +25,7 @@ import java.io.IOException;
/** /**
* Media period that wraps a media source and defers calling its {@link * Media period that wraps a media source and defers calling its {@link
* MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link * MediaSource#createPeriod(MediaPeriodId, Allocator, long)} method until {@link
* #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media * #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media
* period immediately but the media source that should create it is not yet prepared. * period immediately but the media source that should create it is not yet prepared.
*/ */
@ -60,11 +60,14 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
* @param mediaSource The media source to wrap. * @param mediaSource The media source to wrap.
* @param id The identifier used to create the deferred media period. * @param id The identifier used to create the deferred media period.
* @param allocator The allocator used to create the media period. * @param allocator The allocator used to create the media period.
* @param preparePositionUs The expected start position, in microseconds.
*/ */
public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { public DeferredMediaPeriod(
MediaSource mediaSource, MediaPeriodId id, Allocator allocator, long preparePositionUs) {
this.id = id; this.id = id;
this.allocator = allocator; this.allocator = allocator;
this.mediaSource = mediaSource; this.mediaSource = mediaSource;
this.preparePositionUs = preparePositionUs;
preparePositionOverrideUs = C.TIME_UNSET; preparePositionOverrideUs = C.TIME_UNSET;
} }
@ -86,28 +89,25 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
/** /**
* Overrides the default prepare position at which to prepare the media period. This value is only * Overrides the default prepare position at which to prepare the media period. This value is only
* used if the call to {@link MediaPeriod#prepare(Callback, long)} is being deferred. * used if called before {@link #createPeriod(MediaPeriodId)}.
* *
* @param defaultPreparePositionUs The default prepare position to use, in microseconds. * @param preparePositionUs The default prepare position to use, in microseconds.
*/ */
public void overridePreparePositionUs(long defaultPreparePositionUs) { public void overridePreparePositionUs(long preparePositionUs) {
preparePositionOverrideUs = defaultPreparePositionUs; preparePositionOverrideUs = preparePositionUs;
} }
/** /**
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source
* prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()} * then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link
* to release the period. * #releasePeriod()} to release the period.
* *
* @param id The identifier that should be used to create the media period from the media source. * @param id The identifier that should be used to create the media period from the media source.
*/ */
public void createPeriod(MediaPeriodId id) { public void createPeriod(MediaPeriodId id) {
mediaPeriod = mediaSource.createPeriod(id, allocator); long preparePositionUs = getPreparePositionWithOverride(this.preparePositionUs);
mediaPeriod = mediaSource.createPeriod(id, allocator, preparePositionUs);
if (callback != null) { if (callback != null) {
long preparePositionUs =
preparePositionOverrideUs != C.TIME_UNSET
? preparePositionOverrideUs
: this.preparePositionUs;
mediaPeriod.prepare(this, preparePositionUs); mediaPeriod.prepare(this, preparePositionUs);
} }
} }
@ -124,9 +124,8 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
@Override @Override
public void prepare(Callback callback, long preparePositionUs) { public void prepare(Callback callback, long preparePositionUs) {
this.callback = callback; this.callback = callback;
this.preparePositionUs = preparePositionUs;
if (mediaPeriod != null) { if (mediaPeriod != null) {
mediaPeriod.prepare(this, preparePositionUs); mediaPeriod.prepare(this, getPreparePositionWithOverride(this.preparePositionUs));
} }
} }
@ -217,4 +216,9 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
callback.onPrepared(this); callback.onPrepared(this);
} }
private long getPreparePositionWithOverride(long preparePositionUs) {
return preparePositionOverrideUs != C.TIME_UNSET
? preparePositionOverrideUs
: preparePositionUs;
}
} }

View File

@ -20,6 +20,7 @@ import android.os.Handler;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
@ -32,25 +33,13 @@ import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
/** /** @deprecated Use {@link ProgressiveMediaSource} instead. */
* Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}. @Deprecated
* @SuppressWarnings("deprecation")
* <p>If the possible input stream container formats are known, pass a factory that instantiates
* extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to use
* the default extractors. When reading a new stream, the first {@link Extractor} in the array of
* extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be
* used to extract samples from the input stream.
*
* <p>Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking.
*/
public final class ExtractorMediaSource extends BaseMediaSource public final class ExtractorMediaSource extends BaseMediaSource
implements ExtractorMediaPeriod.Listener { implements MediaSource.SourceInfoRefreshListener {
/** /** @deprecated Use {@link MediaSourceEventListener} instead. */
* Listener of {@link ExtractorMediaSource} events.
*
* @deprecated Use {@link MediaSourceEventListener}.
*/
@Deprecated @Deprecated
public interface EventListener { public interface EventListener {
@ -70,7 +59,8 @@ public final class ExtractorMediaSource extends BaseMediaSource
} }
/** Factory for {@link ExtractorMediaSource}s. */ /** Use {@link ProgressiveMediaSource.Factory} instead. */
@Deprecated
public static final class Factory implements AdsMediaSource.MediaSourceFactory { public static final class Factory implements AdsMediaSource.MediaSourceFactory {
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
@ -232,23 +222,11 @@ public final class ExtractorMediaSource extends BaseMediaSource
} }
} }
/** @Deprecated
* The default number of bytes that should be loaded between each each invocation of {@link public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES =
* MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
*/
public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;
private final Uri uri; private final ProgressiveMediaSource progressiveMediaSource;
private final DataSource.Factory dataSourceFactory;
private final ExtractorsFactory extractorsFactory;
private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy;
private final String customCacheKey;
private final int continueLoadingCheckIntervalBytes;
private final @Nullable Object tag;
private long timelineDurationUs;
private boolean timelineIsSeekable;
private @Nullable TransferListener transferListener;
/** /**
* @param uri The {@link Uri} of the media stream. * @param uri The {@link Uri} of the media stream.
@ -261,7 +239,6 @@ public final class ExtractorMediaSource extends BaseMediaSource
* @deprecated Use {@link Factory} instead. * @deprecated Use {@link Factory} instead.
*/ */
@Deprecated @Deprecated
@SuppressWarnings("deprecation")
public ExtractorMediaSource( public ExtractorMediaSource(
Uri uri, Uri uri,
DataSource.Factory dataSourceFactory, DataSource.Factory dataSourceFactory,
@ -284,7 +261,6 @@ public final class ExtractorMediaSource extends BaseMediaSource
* @deprecated Use {@link Factory} instead. * @deprecated Use {@link Factory} instead.
*/ */
@Deprecated @Deprecated
@SuppressWarnings("deprecation")
public ExtractorMediaSource( public ExtractorMediaSource(
Uri uri, Uri uri,
DataSource.Factory dataSourceFactory, DataSource.Factory dataSourceFactory,
@ -317,7 +293,6 @@ public final class ExtractorMediaSource extends BaseMediaSource
* @deprecated Use {@link Factory} instead. * @deprecated Use {@link Factory} instead.
*/ */
@Deprecated @Deprecated
@SuppressWarnings("deprecation")
public ExtractorMediaSource( public ExtractorMediaSource(
Uri uri, Uri uri,
DataSource.Factory dataSourceFactory, DataSource.Factory dataSourceFactory,
@ -347,93 +322,57 @@ public final class ExtractorMediaSource extends BaseMediaSource
@Nullable String customCacheKey, @Nullable String customCacheKey,
int continueLoadingCheckIntervalBytes, int continueLoadingCheckIntervalBytes,
@Nullable Object tag) { @Nullable Object tag) {
this.uri = uri; progressiveMediaSource =
this.dataSourceFactory = dataSourceFactory; new ProgressiveMediaSource(
this.extractorsFactory = extractorsFactory; uri,
this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy; dataSourceFactory,
this.customCacheKey = customCacheKey; extractorsFactory,
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; loadableLoadErrorHandlingPolicy,
this.timelineDurationUs = C.TIME_UNSET; customCacheKey,
this.tag = tag; continueLoadingCheckIntervalBytes,
tag);
} }
@Override @Override
@Nullable @Nullable
public Object getTag() { public Object getTag() {
return tag; return progressiveMediaSource.getTag();
} }
@Override @Override
public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
transferListener = mediaTransferListener; progressiveMediaSource.prepareSource(/* listener= */ this, mediaTransferListener);
notifySourceInfoRefreshed(timelineDurationUs, /* isSeekable= */ false);
} }
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { public void maybeThrowSourceInfoRefreshError() throws IOException {
// Do nothing. progressiveMediaSource.maybeThrowSourceInfoRefreshError();
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
DataSource dataSource = dataSourceFactory.createDataSource(); return progressiveMediaSource.createPeriod(id, allocator, startPositionUs);
if (transferListener != null) {
dataSource.addTransferListener(transferListener);
}
return new ExtractorMediaPeriod(
uri,
dataSource,
extractorsFactory.createExtractors(),
loadableLoadErrorHandlingPolicy,
createEventDispatcher(id),
this,
allocator,
customCacheKey,
continueLoadingCheckIntervalBytes);
} }
@Override @Override
public void releasePeriod(MediaPeriod mediaPeriod) { public void releasePeriod(MediaPeriod mediaPeriod) {
((ExtractorMediaPeriod) mediaPeriod).release(); progressiveMediaSource.releasePeriod(mediaPeriod);
} }
@Override @Override
public void releaseSourceInternal() { public void releaseSourceInternal() {
// Do nothing. progressiveMediaSource.releaseSource(/* listener= */ this);
} }
// ExtractorMediaPeriod.Listener implementation.
@Override @Override
public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) { public void onSourceInfoRefreshed(
// If we already have the duration from a previous source info refresh, use it. MediaSource source, Timeline timeline, @Nullable Object manifest) {
durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; refreshSourceInfo(timeline, manifest);
if (timelineDurationUs == durationUs && timelineIsSeekable == isSeekable) {
// Suppress no-op source info changes.
return;
}
notifySourceInfoRefreshed(durationUs, isSeekable);
} }
// Internal methods.
private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) {
timelineDurationUs = durationUs;
timelineIsSeekable = isSeekable;
// TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223.
refreshSourceInfo(
new SinglePeriodTimeline(
timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, tag),
/* manifest= */ null);
}
/**
* Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in
* {@link MediaSourceEventListener}.
*/
@Deprecated @Deprecated
@SuppressWarnings("deprecation")
private static final class EventListenerWrapper extends DefaultMediaSourceEventListener { private static final class EventListenerWrapper extends DefaultMediaSourceEventListener {
private final EventListener eventListener; private final EventListener eventListener;
public EventListenerWrapper(EventListener eventListener) { public EventListenerWrapper(EventListener eventListener) {

View File

@ -31,7 +31,7 @@ import java.util.Map;
* Loops a {@link MediaSource} a specified number of times. * Loops a {@link MediaSource} a specified number of times.
* *
* <p>Note: To loop a {@link MediaSource} indefinitely, it is usually better to use {@link * <p>Note: To loop a {@link MediaSource} indefinitely, it is usually better to use {@link
* ExoPlayer#setRepeatMode(int)}. * ExoPlayer#setRepeatMode(int)} instead of this class.
*/ */
public final class LoopingMediaSource extends CompositeMediaSource<Void> { public final class LoopingMediaSource extends CompositeMediaSource<Void> {
@ -77,14 +77,15 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> {
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
if (loopCount == Integer.MAX_VALUE) { if (loopCount == Integer.MAX_VALUE) {
return childSource.createPeriod(id, allocator); return childSource.createPeriod(id, allocator, startPositionUs);
} }
Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid); Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid);
MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid); MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid);
childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id); childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id);
MediaPeriod mediaPeriod = childSource.createPeriod(childMediaPeriodId, allocator); MediaPeriod mediaPeriod =
childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId); mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId);
return mediaPeriod; return mediaPeriod;
} }

View File

@ -87,18 +87,18 @@ public interface MediaPeriod extends SequenceableLoader {
TrackGroupArray getTrackGroups(); TrackGroupArray getTrackGroups();
/** /**
* Returns a list of {@link StreamKey stream keys} which allow to filter the media in this period * Returns a list of {@link StreamKey StreamKeys} which allow to filter the media in this period
* to load only the parts needed to play the provided {@link TrackSelection}. * to load only the parts needed to play the provided {@link TrackSelection TrackSelections}.
* *
* <p>This method is only called after the period has been prepared. * <p>This method is only called after the period has been prepared.
* *
* @param trackSelection The {@link TrackSelection} describing the tracks for which stream keys * @param trackSelections The {@link TrackSelection TrackSelections} describing the tracks for
* are requested. * which stream keys are requested.
* @return The corresponding {@link StreamKey stream keys} for the selected tracks, or an empty * @return The corresponding {@link StreamKey StreamKeys} for the selected tracks, or an empty
* list if filtering is not possible and the entire media needs to be loaded to play the * list if filtering is not possible and the entire media needs to be loaded to play the
* selected tracks. * selected tracks.
*/ */
default List<StreamKey> getStreamKeys(TrackSelection trackSelection) { default List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {
return Collections.emptyList(); return Collections.emptyList();
} }

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
@ -34,8 +35,8 @@ import java.io.IOException;
* on the {@link SourceInfoRefreshListener}s passed to {@link * on the {@link SourceInfoRefreshListener}s passed to {@link
* #prepareSource(SourceInfoRefreshListener, TransferListener)}. * #prepareSource(SourceInfoRefreshListener, TransferListener)}.
* <li>To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are * <li>To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are
* obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a
* the player to load and read the media. * way for the player to load and read the media.
* </ul> * </ul>
* *
* All methods are called on the player's internal playback thread, as described in the {@link * All methods are called on the player's internal playback thread, as described in the {@link
@ -89,12 +90,10 @@ public interface MediaSource {
public final long windowSequenceNumber; public final long windowSequenceNumber;
/** /**
* The end position to which the media period's content is clipped in order to play a following * The index of the next ad group to which the media period's content is clipped, or {@link
* ad group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if * C#INDEX_UNSET} if there is no following ad group or if this media period is an ad.
* this media period is an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll
* ad follows at the end of this content media period.
*/ */
public final long endPositionUs; public final int nextAdGroupIndex;
/** /**
* Creates a media period identifier for a dummy period which is not part of a buffered sequence * Creates a media period identifier for a dummy period which is not part of a buffered sequence
@ -103,7 +102,7 @@ public interface MediaSource {
* @param periodUid The unique id of the timeline period. * @param periodUid The unique id of the timeline period.
*/ */
public MediaPeriodId(Object periodUid) { public MediaPeriodId(Object periodUid) {
this(periodUid, C.INDEX_UNSET); this(periodUid, /* windowSequenceNumber= */ C.INDEX_UNSET);
} }
/** /**
@ -114,7 +113,12 @@ public interface MediaSource {
* windows this media period is part of. * windows this media period is part of.
*/ */
public MediaPeriodId(Object periodUid, long windowSequenceNumber) { public MediaPeriodId(Object periodUid, long windowSequenceNumber) {
this(periodUid, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, C.TIME_UNSET); this(
periodUid,
/* adGroupIndex= */ C.INDEX_UNSET,
/* adIndexInAdGroup= */ C.INDEX_UNSET,
windowSequenceNumber,
/* nextAdGroupIndex= */ C.INDEX_UNSET);
} }
/** /**
@ -123,11 +127,16 @@ public interface MediaSource {
* @param periodUid The unique id of the timeline period. * @param periodUid The unique id of the timeline period.
* @param windowSequenceNumber The sequence number of the window in the buffered sequence of * @param windowSequenceNumber The sequence number of the window in the buffered sequence of
* windows this media period is part of. * windows this media period is part of.
* @param endPositionUs The end position of the media period within the timeline period, in * @param nextAdGroupIndex The index of the next ad group to which the media period's content is
* microseconds. * clipped.
*/ */
public MediaPeriodId(Object periodUid, long windowSequenceNumber, long endPositionUs) { public MediaPeriodId(Object periodUid, long windowSequenceNumber, int nextAdGroupIndex) {
this(periodUid, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, endPositionUs); this(
periodUid,
/* adGroupIndex= */ C.INDEX_UNSET,
/* adIndexInAdGroup= */ C.INDEX_UNSET,
windowSequenceNumber,
nextAdGroupIndex);
} }
/** /**
@ -142,7 +151,12 @@ public interface MediaSource {
*/ */
public MediaPeriodId( public MediaPeriodId(
Object periodUid, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) { Object periodUid, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) {
this(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, C.TIME_UNSET); this(
periodUid,
adGroupIndex,
adIndexInAdGroup,
windowSequenceNumber,
/* nextAdGroupIndex= */ C.INDEX_UNSET);
} }
private MediaPeriodId( private MediaPeriodId(
@ -150,12 +164,12 @@ public interface MediaSource {
int adGroupIndex, int adGroupIndex,
int adIndexInAdGroup, int adIndexInAdGroup,
long windowSequenceNumber, long windowSequenceNumber,
long endPositionUs) { int nextAdGroupIndex) {
this.periodUid = periodUid; this.periodUid = periodUid;
this.adGroupIndex = adGroupIndex; this.adGroupIndex = adGroupIndex;
this.adIndexInAdGroup = adIndexInAdGroup; this.adIndexInAdGroup = adIndexInAdGroup;
this.windowSequenceNumber = windowSequenceNumber; this.windowSequenceNumber = windowSequenceNumber;
this.endPositionUs = endPositionUs; this.nextAdGroupIndex = nextAdGroupIndex;
} }
/** Returns a copy of this period identifier but with {@code newPeriodUid} as its period uid. */ /** Returns a copy of this period identifier but with {@code newPeriodUid} as its period uid. */
@ -163,7 +177,7 @@ public interface MediaSource {
return periodUid.equals(newPeriodUid) return periodUid.equals(newPeriodUid)
? this ? this
: new MediaPeriodId( : new MediaPeriodId(
newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, endPositionUs); newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, nextAdGroupIndex);
} }
/** /**
@ -187,7 +201,7 @@ public interface MediaSource {
&& adGroupIndex == periodId.adGroupIndex && adGroupIndex == periodId.adGroupIndex
&& adIndexInAdGroup == periodId.adIndexInAdGroup && adIndexInAdGroup == periodId.adIndexInAdGroup
&& windowSequenceNumber == periodId.windowSequenceNumber && windowSequenceNumber == periodId.windowSequenceNumber
&& endPositionUs == periodId.endPositionUs; && nextAdGroupIndex == periodId.nextAdGroupIndex;
} }
@Override @Override
@ -197,7 +211,7 @@ public interface MediaSource {
result = 31 * result + adGroupIndex; result = 31 * result + adGroupIndex;
result = 31 * result + adIndexInAdGroup; result = 31 * result + adIndexInAdGroup;
result = 31 * result + (int) windowSequenceNumber; result = 31 * result + (int) windowSequenceNumber;
result = 31 * result + (int) endPositionUs; result = 31 * result + nextAdGroupIndex;
return result; return result;
} }
} }
@ -224,7 +238,6 @@ public interface MediaSource {
default Object getTag() { default Object getTag() {
return null; return null;
} }
/** /**
* Starts source preparation if not yet started, and adds a listener for timeline and/or manifest * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest
* updates. * updates.
@ -243,8 +256,7 @@ public interface MediaSource {
* and other data. * and other data.
*/ */
void prepareSource( void prepareSource(
SourceInfoRefreshListener listener, SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener);
@Nullable TransferListener mediaTransferListener);
/** /**
* Throws any pending error encountered while loading or refreshing source information. * Throws any pending error encountered while loading or refreshing source information.
@ -261,9 +273,10 @@ public interface MediaSource {
* *
* @param id The identifier of the period. * @param id The identifier of the period.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param startPositionUs The expected start position, in microseconds.
* @return A new {@link MediaPeriod}. * @return A new {@link MediaPeriod}.
*/ */
MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator); MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs);
/** /**
* Releases the period. * Releases the period.

View File

@ -120,13 +120,13 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
MediaPeriod[] periods = new MediaPeriod[mediaSources.length]; MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid); int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
for (int i = 0; i < periods.length; i++) { for (int i = 0; i < periods.length; i++) {
MediaPeriodId childMediaPeriodId = MediaPeriodId childMediaPeriodId =
id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex)); id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator); periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs);
} }
return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods); return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods);
} }

View File

@ -54,11 +54,12 @@ import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /** A {@link MediaPeriod} that extracts data using an {@link Extractor}. */
* A {@link MediaPeriod} that extracts data using an {@link Extractor}. /* package */ final class ProgressiveMediaPeriod
*/ implements MediaPeriod,
/* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput, ExtractorOutput,
Loader.Callback<ExtractorMediaPeriod.ExtractingLoadable>, Loader.ReleaseCallback, Loader.Callback<ProgressiveMediaPeriod.ExtractingLoadable>,
Loader.ReleaseCallback,
UpstreamFormatChangedListener { UpstreamFormatChangedListener {
/** /**
@ -145,7 +146,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
"nullness:argument.type.incompatible", "nullness:argument.type.incompatible",
"nullness:methodref.receiver.bound.invalid" "nullness:methodref.receiver.bound.invalid"
}) })
public ExtractorMediaPeriod( public ProgressiveMediaPeriod(
Uri uri, Uri uri,
DataSource dataSource, DataSource dataSource,
Extractor[] extractors, Extractor[] extractors,
@ -163,14 +164,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
this.allocator = allocator; this.allocator = allocator;
this.customCacheKey = customCacheKey; this.customCacheKey = customCacheKey;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
loader = new Loader("Loader:ExtractorMediaPeriod"); loader = new Loader("Loader:ProgressiveMediaPeriod");
extractorHolder = new ExtractorHolder(extractors); extractorHolder = new ExtractorHolder(extractors);
loadCondition = new ConditionVariable(); loadCondition = new ConditionVariable();
maybeFinishPrepareRunnable = this::maybeFinishPrepare; maybeFinishPrepareRunnable = this::maybeFinishPrepare;
onContinueLoadingRequestedRunnable = onContinueLoadingRequestedRunnable =
() -> { () -> {
if (!released) { if (!released) {
Assertions.checkNotNull(callback).onContinueLoadingRequested(ExtractorMediaPeriod.this); Assertions.checkNotNull(callback)
.onContinueLoadingRequested(ProgressiveMediaPeriod.this);
} }
}; };
handler = new Handler(); handler = new Handler();
@ -356,18 +358,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} else if (isPendingReset()) { } else if (isPendingReset()) {
return pendingResetPositionUs; return pendingResetPositionUs;
} }
long largestQueuedTimestampUs; long largestQueuedTimestampUs = Long.MAX_VALUE;
if (haveAudioVideoTracks) { if (haveAudioVideoTracks) {
// Ignore non-AV tracks, which may be sparse or poorly interleaved. // Ignore non-AV tracks, which may be sparse or poorly interleaved.
largestQueuedTimestampUs = Long.MAX_VALUE;
int trackCount = sampleQueues.length; int trackCount = sampleQueues.length;
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
if (trackIsAudioVideoFlags[i]) { if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) {
largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs, largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
sampleQueues[i].getLargestQueuedTimestampUs()); sampleQueues[i].getLargestQueuedTimestampUs());
} }
} }
} else { }
if (largestQueuedTimestampUs == Long.MAX_VALUE) {
largestQueuedTimestampUs = getLargestQueuedTimestampUs(); largestQueuedTimestampUs = getLargestQueuedTimestampUs();
} }
return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs
@ -851,23 +853,23 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Override @Override
public boolean isReady() { public boolean isReady() {
return ExtractorMediaPeriod.this.isReady(track); return ProgressiveMediaPeriod.this.isReady(track);
} }
@Override @Override
public void maybeThrowError() throws IOException { public void maybeThrowError() throws IOException {
ExtractorMediaPeriod.this.maybeThrowError(); ProgressiveMediaPeriod.this.maybeThrowError();
} }
@Override @Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
boolean formatRequired) { boolean formatRequired) {
return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired); return ProgressiveMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired);
} }
@Override @Override
public int skipData(long positionUs) { public int skipData(long positionUs) {
return ExtractorMediaPeriod.this.skipData(track, positionUs); return ProgressiveMediaPeriod.this.skipData(track, positionUs);
} }
} }
@ -988,7 +990,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
position, position,
C.LENGTH_UNSET, C.LENGTH_UNSET,
customCacheKey, customCacheKey,
DataSpec.FLAG_ALLOW_ICY_METADATA | DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN); DataSpec.FLAG_ALLOW_ICY_METADATA
| DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN
| DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);
} }
private void setLoadPosition(long position, long timeUs) { private void setLoadPosition(long position, long timeUs) {

View File

@ -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);
}
}

View File

@ -57,6 +57,7 @@ import com.google.android.exoplayer2.util.Util;
private long largestDiscardedTimestampUs; private long largestDiscardedTimestampUs;
private long largestQueuedTimestampUs; private long largestQueuedTimestampUs;
private boolean isLastSampleQueued;
private boolean upstreamKeyframeRequired; private boolean upstreamKeyframeRequired;
private boolean upstreamFormatRequired; private boolean upstreamFormatRequired;
private Format upstreamFormat; private Format upstreamFormat;
@ -93,6 +94,7 @@ import com.google.android.exoplayer2.util.Util;
upstreamKeyframeRequired = true; upstreamKeyframeRequired = true;
largestDiscardedTimestampUs = Long.MIN_VALUE; largestDiscardedTimestampUs = Long.MIN_VALUE;
largestQueuedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE;
isLastSampleQueued = false;
if (resetUpstreamFormat) { if (resetUpstreamFormat) {
upstreamFormat = null; upstreamFormat = null;
upstreamFormatRequired = true; upstreamFormatRequired = true;
@ -118,6 +120,7 @@ import com.google.android.exoplayer2.util.Util;
Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition));
length -= discardCount; length -= discardCount;
largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length));
isLastSampleQueued = discardCount == 0 && isLastSampleQueued;
if (length == 0) { if (length == 0) {
return 0; return 0;
} else { } else {
@ -186,6 +189,19 @@ import com.google.android.exoplayer2.util.Util;
return largestQueuedTimestampUs; return largestQueuedTimestampUs;
} }
/**
* Returns whether the last sample of the stream has knowingly been queued. A return value of
* {@code false} means that the last sample had not been queued or that it's unknown whether the
* last sample has been queued.
*
* <p>Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
* considered as having been queued. Samples that were dequeued from the front of the queue are
* considered as having been queued.
*/
public synchronized boolean isLastSampleQueued() {
return isLastSampleQueued;
}
/** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
public synchronized long getFirstTimestampUs() { public synchronized long getFirstTimestampUs() {
return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex];
@ -224,7 +240,7 @@ import com.google.android.exoplayer2.util.Util;
boolean formatRequired, boolean loadingFinished, Format downstreamFormat, boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
SampleExtrasHolder extrasHolder) { SampleExtrasHolder extrasHolder) {
if (!hasNextSample()) { if (!hasNextSample()) {
if (loadingFinished) { if (loadingFinished || isLastSampleQueued) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ; return C.RESULT_BUFFER_READ;
} else if (upstreamFormat != null } else if (upstreamFormat != null
@ -388,7 +404,9 @@ import com.google.android.exoplayer2.util.Util;
upstreamKeyframeRequired = false; upstreamKeyframeRequired = false;
} }
Assertions.checkState(!upstreamFormatRequired); Assertions.checkState(!upstreamFormatRequired);
commitSampleTimestamp(timeUs);
isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0;
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
int relativeEndIndex = getRelativeIndex(length); int relativeEndIndex = getRelativeIndex(length);
timesUs[relativeEndIndex] = timeUs; timesUs[relativeEndIndex] = timeUs;
@ -439,10 +457,6 @@ import com.google.android.exoplayer2.util.Util;
} }
} }
public synchronized void commitSampleTimestamp(long timeUs) {
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
}
/** /**
* Attempts to discard samples from the end of the queue to allow samples starting from the * Attempts to discard samples from the end of the queue to allow samples starting from the
* specified timestamp to be spliced in. Samples will not be discarded prior to the read position. * specified timestamp to be spliced in. Samples will not be discarded prior to the read position.

View File

@ -224,6 +224,15 @@ public class SampleQueue implements TrackOutput {
return metadataQueue.getLargestQueuedTimestampUs(); return metadataQueue.getLargestQueuedTimestampUs();
} }
/**
* Returns whether the last sample of the stream has knowingly been queued. A return value of
* {@code false} means that the last sample had not been queued or that it's unknown whether the
* last sample has been queued.
*/
public boolean isLastSampleQueued() {
return metadataQueue.isLastSampleQueued();
}
/** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
public long getFirstTimestampUs() { public long getFirstTimestampUs() {
return metadataQueue.getFirstTimestampUs(); return metadataQueue.getFirstTimestampUs();

Some files were not shown because too many files have changed in this diff Show More