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-project.txt
# Bazel
bazel-bin
bazel-genfiles
bazel-out
bazel-testlogs
# Other
.DS_Store
cmake-build-debug
@ -66,3 +72,6 @@ extensions/cronet/jniLibs/*
extensions/cronet/libs/*
!extensions/cronet/libs/README.md
# Cast receiver
cast_receiver_app/external-js
cast_receiver_app/bazel-cast_receiver_app

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
@ -483,7 +483,7 @@ public class PlayerActivity extends Activity
.setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
default: {
throw new IllegalStateException("Unsupported type: " + type);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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
* yet.
* yet or the current player is idle.
*
* @param player The {@link Player} for which data will be collected.
*/
public void setPlayer(Player player) {
Assertions.checkState(this.player == null);
Assertions.checkState(
this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty());
this.player = Assertions.checkNotNull(player);
}
@ -488,7 +489,10 @@ public class AnalyticsCollector
@Override
public final void onPlayerError(ExoPlaybackException error) {
EventTime eventTime = generatePlayingMediaPeriodEventTime();
EventTime eventTime =
error.type == ExoPlaybackException.TYPE_SOURCE
? generateLoadingMediaPeriodEventTime()
: generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlayerError(eventTime, error);
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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);
Collections.sort(mutableKeys);
this.keys = Collections.unmodifiableList(mutableKeys);
this.data = data != null ? data : Util.EMPTY_BYTE_ARRAY;
this.data = data != null ? Arrays.copyOf(data, data.length) : Util.EMPTY_BYTE_ARRAY;
}
}

View File

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

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

View File

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

View File

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

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
: new FileDataSourceFactory();
DataSink.Factory writeDataSinkFactory =
cacheWriteDataSinkFactory != null
? cacheWriteDataSinkFactory
: new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_MAX_CACHE_FILE_SIZE);
if (cacheWriteDataSinkFactory == null) {
cacheWriteDataSinkFactory =
new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE);
}
onlineCacheDataSourceFactory =
new CacheDataSourceFactory(
cache,
upstreamFactory,
readDataSourceFactory,
writeDataSinkFactory,
cacheWriteDataSinkFactory,
CacheDataSource.FLAG_BLOCK_ON_CACHE,
/* eventListener= */ null,
cacheKeyFactory);

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) {
this.dataSpec =
new DataSpec(
uri, /* absoluteStreamPosition= */ 0, C.LENGTH_UNSET, customCacheKey, /* flags= */ 0);
uri,
/* absoluteStreamPosition= */ 0,
C.LENGTH_UNSET,
customCacheKey,
/* flags= */ DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);
this.cache = constructorHelper.getCache();
this.dataSource = constructorHelper.createCacheDataSource();
this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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