Merge pull request #3 from google/dev-v2

Merge pull request #3 from google/dev-v2
This commit is contained in:
aujohn 2019-01-23 16:39:20 -08:00 committed by GitHub
commit 20aeb0c8aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
239 changed files with 10181 additions and 3495 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

@ -5,13 +5,90 @@
* 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)).
* Add options for controlling audio track selections to `DefaultTrackSelector`
([#3314](https://github.com/google/ExoPlayer/issues/3314)).
* Track selection:
* Add options for controlling audio track selections to `DefaultTrackSelector`
([#3314](https://github.com/google/ExoPlayer/issues/3314)).
* Update `TrackSelection.Factory` interface to support creating all track
selections together.
* Do not retry failed loads whose error is `FileNotFoundException`.
* Prevent Cea608Decoder from generating Subtitles with null Cues list
* Caching: 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 (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
* Offline:
* Speed up removal of segmented downloads
([#5136](https://github.com/google/ExoPlayer/issues/5136)).
* Add `setStreamKeys` method to factories of DASH, SmoothStreaming and HLS
media sources to simplify filtering by downloaded streams.
* Caching:
* 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
(`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
* DownloadManager:
* Create only one task for all DownloadActions for the same content.
* 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)).
* 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.
* Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a
callback `Runnable`.
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.
* Change signature of `PlayerNotificationManager.NotificationListener` to better
fit service requirements. Remove ability to set a custom stop action.
* Add workaround for video quality problems with Amlogic decoders
([#5003](https://github.com/google/ExoPlayer/issues/5003)).
* Associate fatal player errors of type SOURCE with the loading source in
`AnalyticsListener.EventTime`
([#5407](https://github.com/google/ExoPlayer/issues/5407)).
### 2.9.4 ###
* IMA extension: Clear ads loader listeners on release
([#4114](https://github.com/google/ExoPlayer/issues/4114)).
* SmoothStreaming: Fix support for subtitles in DRM protected streams
([#5378](https://github.com/google/ExoPlayer/issues/5378)).
* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior
of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)).
* GVR extension: upgrade GVR SDK dependency to 1.190.0.
* Fix issue where sending callbacks for playlist changes may cause problems
because of parallel player access
([#5240](https://github.com/google/ExoPlayer/issues/5240)).
* 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)).
* 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`.
### 2.9.3 ###
* Captions: Support PNG subtitles in SMPTE-TT
([#1583](https://github.com/google/ExoPlayer/issues/1583)).
* MPEG-TS: Use random access indicators to minimize the need for
`FLAG_ALLOW_NON_IDR_KEYFRAMES`.
* Downloading: Reduce time taken to remove downloads
([#5136](https://github.com/google/ExoPlayer/issues/5136)).
* MP3:
* Use the true bitrate for constant-bitrate MP3 seeking.
* Fix issue where streams would play twice on some Samsung devices
([#4519](https://github.com/google/ExoPlayer/issues/4519)).
* Fix regression where some audio formats were incorrectly marked as being
unplayable due to under-reporting of platform decoder capabilities
([#5145](https://github.com/google/ExoPlayer/issues/5145)).
* Fix decode-only frame skipping on Nvidia Shield TV devices.
* Workaround for MiTV (dangal) issue when swapping output surface
([#5169](https://github.com/google/ExoPlayer/issues/5169)).
### 2.9.2 ###
@ -60,10 +137,10 @@
* DASH: Parse ProgramInformation element if present in the manifest.
* HLS:
* Add constructor to `DefaultHlsExtractorFactory` for adding TS payload
reader factory flags.
reader factory flags
([#4861](https://github.com/google/ExoPlayer/issues/4861)).
* Fix bug in segment sniffing
([#5039](https://github.com/google/ExoPlayer/issues/5039)).
([#4861](https://github.com/google/ExoPlayer/issues/4861)).
* SubRip: Add support for alignment tags, and remove tags from the displayed
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
* Fix issue with blind seeking to windows with non-zero offset in a
@ -1125,7 +1202,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.
@ -1369,8 +1446,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,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.9.2'
releaseVersionCode = 2009002
releaseVersion = '2.9.4'
releaseVersionCode = 2009004
// 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

View File

@ -49,6 +49,16 @@ android {
disable 'MissingTranslation'
}
flavorDimensions "receiver"
productFlavors {
defaultCast {
dimension "receiver"
manifestPlaceholders =
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
}
}
}
dependencies {

View File

@ -23,7 +23,7 @@
android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider" />
android:value="${castOptionsProvider}" />
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"

View File

@ -268,7 +268,7 @@ import java.util.ArrayList;
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex();
if (timeline.isEmpty()) {
if (currentPlayer == castPlayer && timeline.isEmpty()) {
castMediaQueueCreationPending = true;
}
}

View File

@ -15,59 +15,99 @@
*/
package com.google.android.exoplayer2.castdemo;
import com.google.android.exoplayer2.ext.cast.MediaItem;
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 {
/** Represents a media sample. */
public static final class Sample {
/** The uri of the media content. */
public final String uri;
/** The name of the sample. */
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}.
* @param name See {@link #name}.
* @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
public String toString() {
return name;
}
}
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
/** The list of samples available in the cast demo app. */
public static final List<MediaItem> SAMPLES;
public static final List<Sample> SAMPLES;
static {
// App samples.
ArrayList<MediaItem> samples = new ArrayList<>();
MediaItem.Builder sampleBuilder = new MediaItem.Builder();
ArrayList<Sample> samples = new ArrayList<>();
samples.add(
sampleBuilder
.setTitle("DASH (clear,MP4,H264)")
.setMimeType(MIME_TYPE_DASH)
.setMedia("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
.buildAndClear());
new Sample(
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
"Clear DASH: Tears",
MIME_TYPE_DASH));
samples.add(
sampleBuilder
.setTitle("Tears of Steel (HLS)")
.setMimeType(MIME_TYPE_HLS)
.setMedia(
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
+ "hls/TearsOfSteel.m3u8")
.buildAndClear());
new Sample(
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
+ "hls/TearsOfSteel.m3u8",
"Clear HLS: Tears of Steel",
MIME_TYPE_HLS));
samples.add(
sampleBuilder
.setTitle("HLS Basic (TS)")
.setMimeType(MIME_TYPE_HLS)
.setMedia(
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3"
+ "/bipbop_4x3_variant.m3u8")
.buildAndClear());
new Sample(
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3"
+ "/bipbop_4x3_variant.m3u8",
"Clear HLS: Basic 4x3",
MIME_TYPE_HLS));
samples.add(
sampleBuilder
.setTitle("Dizzy (MP4)")
.setMimeType(MIME_TYPE_VIDEO_MP4)
.setMedia("https://html5demos.com/assets/dizzy.mp4")
.buildAndClear());
new Sample(
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4));
SAMPLES = Collections.unmodifiableList(samples);
}

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.graphics.ColorUtils;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
@ -42,6 +41,8 @@ import com.google.android.exoplayer2.ui.PlayerView;
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
@ -50,6 +51,8 @@ import com.google.android.gms.cast.framework.CastContext;
public class MainActivity extends AppCompatActivity
implements OnClickListener, PlayerManager.QueuePositionListener {
private final MediaItem.Builder mediaItemBuilder;
private PlayerView localPlayerView;
private PlayerControlView castControlView;
private PlayerManager playerManager;
@ -57,13 +60,30 @@ public class MainActivity extends AppCompatActivity
private MediaQueueListAdapter mediaQueueListAdapter;
private CastContext castContext;
public MainActivity() {
mediaItemBuilder = new MediaItem.Builder();
}
// Activity lifecycle methods.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Getting the cast context later than onStart can cause device discovery not to take place.
castContext = CastContext.getSharedInstance(this);
try {
castContext = CastContext.getSharedInstance(this);
} catch (RuntimeException e) {
Throwable cause = e.getCause();
while (cause != null) {
if (cause instanceof DynamiteModule.LoadingException) {
setContentView(R.layout.cast_context_error_message_layout);
return;
}
cause = cause.getCause();
}
// Unknown error. We propagate it.
throw e;
}
setContentView(R.layout.main_activity);
@ -93,6 +113,10 @@ public class MainActivity extends AppCompatActivity
@Override
public void onResume() {
super.onResume();
if (castContext == null) {
// There is no Cast context to work with. Do nothing.
return;
}
String applicationId = castContext.getCastOptions().getReceiverApplicationId();
switch (applicationId) {
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
@ -113,6 +137,10 @@ public class MainActivity extends AppCompatActivity
@Override
public void onPause() {
super.onPause();
if (castContext == null) {
// Nothing to release.
return;
}
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
mediaQueueList.setAdapter(null);
playerManager.release();
@ -154,7 +182,19 @@ public class MainActivity extends AppCompatActivity
sampleList.setAdapter(new SampleListAdapter(this));
sampleList.setOnItemClickListener(
(parent, view, position, id) -> {
playerManager.addItem(DemoUtil.SAMPLES.get(position));
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position);
mediaItemBuilder
.clear()
.setMedia(sample.uri)
.setTitle(sample.name)
.setMimeType(sample.mimeType);
if (sample.drmSchemeUuid != null) {
mediaItemBuilder.setDrmSchemes(
Collections.singletonList(
new MediaItem.DrmScheme(
sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri))));
}
playerManager.addItem(mediaItemBuilder.build());
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
});
return dialogList;
@ -254,19 +294,11 @@ public class MainActivity extends AppCompatActivity
}
private static final class SampleListAdapter extends ArrayAdapter<MediaItem> {
private static final class SampleListAdapter extends ArrayAdapter<DemoUtil.Sample> {
public SampleListAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
}
@Override
public View getView(int position, @Nullable View convertView, ViewGroup parent) {
TextView view = (TextView) super.getView(position, convertView, parent);
MediaItem sample = DemoUtil.SAMPLES.get(position);
view.setText(sample.title);
return view;
}
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="20sp"
android:gravity="center"
android:text="@string/cast_context_error"/>
</LinearLayout>

View File

@ -22,4 +22,6 @@
<string name="sample_list_dialog_title">Add samples</string>
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
</resources>

View File

@ -16,6 +16,8 @@
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.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
@ -72,6 +74,17 @@ public class DemoApplication extends Application {
return "withExtensions".equals(BuildConfig.FLAVOR);
}
public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
@DefaultRenderersFactory.ExtensionRendererMode
int extensionRendererMode =
useExtensionRenderers()
? (preferExtensionRenderer
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
return new DefaultRenderersFactory(this, extensionRendererMode);
}
public DownloadManager getDownloadManager() {
initDownloadManager();
return downloadManager;
@ -88,10 +101,12 @@ public class DemoApplication extends Application {
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,

View File

@ -17,8 +17,8 @@ package com.google.android.exoplayer2.demo;
import android.app.Notification;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
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.util.NotificationUtil;
@ -31,12 +31,15 @@ public class DemoDownloadService extends DownloadService {
private static final int JOB_ID = 1;
private static final int FOREGROUND_NOTIFICATION_ID = 1;
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
public DemoDownloadService() {
super(
FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
CHANNEL_ID,
R.string.exo_download_notification_channel_name);
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
}
@Override
@ -50,40 +53,38 @@ public class DemoDownloadService extends DownloadService {
}
@Override
protected Notification getForegroundNotification(TaskState[] taskStates) {
protected Notification getForegroundNotification(DownloadState[] downloadStates) {
return DownloadNotificationUtil.buildProgressNotification(
/* context= */ this,
R.drawable.exo_controls_play,
R.drawable.ic_download,
CHANNEL_ID,
/* contentIntent= */ null,
/* message= */ null,
taskStates);
downloadStates);
}
@Override
protected void onTaskStateChanged(TaskState taskState) {
if (taskState.action.isRemoveAction) {
return;
}
protected void onDownloadStateChanged(DownloadState downloadState) {
Notification notification = null;
if (taskState.state == TaskState.STATE_COMPLETED) {
if (downloadState.state == DownloadState.STATE_COMPLETED) {
notification =
DownloadNotificationUtil.buildDownloadCompletedNotification(
/* context= */ this,
R.drawable.exo_controls_play,
R.drawable.ic_download_done,
CHANNEL_ID,
/* contentIntent= */ null,
Util.fromUtf8Bytes(taskState.action.data));
} else if (taskState.state == TaskState.STATE_FAILED) {
Util.fromUtf8Bytes(downloadState.customMetadata));
} else if (downloadState.state == DownloadState.STATE_FAILED) {
notification =
DownloadNotificationUtil.buildDownloadFailedNotification(
/* context= */ this,
R.drawable.exo_controls_play,
R.drawable.ic_download_done,
CHANNEL_ID,
/* contentIntent= */ null,
Util.fromUtf8Bytes(taskState.action.data));
Util.fromUtf8Bytes(downloadState.customMetadata));
} else {
return;
}
int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId;
NotificationUtil.setNotification(this, notificationId, notification);
NotificationUtil.setNotification(this, nextNotificationId++, notification);
}
}

View File

@ -19,37 +19,44 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.Nullable;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
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.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
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.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey;
import com.google.android.exoplayer2.source.TrackGroup;
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;
import com.google.android.exoplayer2.ui.DefaultTrackNameProvider;
import com.google.android.exoplayer2.ui.TrackNameProvider;
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.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -114,14 +121,19 @@ public class DownloadTracker implements DownloadManager.Listener {
return trackedDownloadStates.get(uri).getKeys();
}
public void toggleDownload(Activity activity, String name, Uri uri, String extension) {
public void toggleDownload(
Activity activity,
String name,
Uri uri,
String extension,
RenderersFactory renderersFactory) {
if (isDownloaded(uri)) {
DownloadAction removeAction = getDownloadHelper(uri, extension).getRemoveAction();
DownloadAction removeAction =
getDownloadHelper(uri, extension, renderersFactory).getRemoveAction();
startServiceWithAction(removeAction);
} else {
StartDownloadDialogHelper helper =
new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name);
helper.prepare();
new StartDownloadDialogHelper(
activity, getDownloadHelper(uri, extension, renderersFactory), name);
}
}
@ -133,13 +145,11 @@ public class DownloadTracker implements DownloadManager.Listener {
}
@Override
public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
DownloadAction action = taskState.action;
Uri uri = action.uri;
if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED)
|| (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) {
public void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState) {
if (downloadState.state == DownloadState.STATE_REMOVED
|| downloadState.state == DownloadState.STATE_FAILED) {
// A download has been removed, or has failed. Stop tracking it.
if (trackedDownloadStates.remove(uri) != null) {
if (trackedDownloadStates.remove(downloadState.uri) != null) {
handleTrackedDownloadStatesChanged();
}
}
@ -150,6 +160,14 @@ 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() {
@ -192,15 +210,16 @@ public class DownloadTracker implements DownloadManager.Listener {
DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
}
private DownloadHelper getDownloadHelper(Uri uri, String extension) {
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);
return new DashDownloadHelper(uri, dataSourceFactory, renderersFactory);
case C.TYPE_SS:
return new SsDownloadHelper(uri, dataSourceFactory);
return new SsDownloadHelper(uri, dataSourceFactory, renderersFactory);
case C.TYPE_HLS:
return new HlsDownloadHelper(uri, dataSourceFactory);
return new HlsDownloadHelper(uri, dataSourceFactory, renderersFactory);
case C.TYPE_OTHER:
return new ProgressiveDownloadHelper(uri);
default:
@ -208,84 +227,165 @@ public class DownloadTracker implements DownloadManager.Listener {
}
}
@SuppressWarnings("UngroupedOverloads")
private final class StartDownloadDialogHelper
implements DownloadHelper.Callback, DialogInterface.OnClickListener {
implements DownloadHelper.Callback,
DialogInterface.OnClickListener,
View.OnClickListener,
TrackSelectionView.DialogCallback {
private final DownloadHelper downloadHelper;
private final DownloadHelper<?> downloadHelper;
private final String name;
private final LayoutInflater dialogInflater;
private final AlertDialog dialog;
private final LinearLayout selectionList;
private final AlertDialog.Builder builder;
private final View dialogView;
private final List<TrackKey> trackKeys;
private final ArrayAdapter<String> trackTitles;
private final ListView representationList;
private MappedTrackInfo mappedTrackInfo;
private DefaultTrackSelector.Parameters parameters;
public StartDownloadDialogHelper(
Activity activity, DownloadHelper downloadHelper, String name) {
private StartDownloadDialogHelper(
Activity activity, DownloadHelper<?> downloadHelper, String name) {
this.downloadHelper = downloadHelper;
this.name = name;
builder =
AlertDialog.Builder builder =
new AlertDialog.Builder(activity)
.setTitle(R.string.exo_download_description)
.setTitle(R.string.download_preparing)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, null);
// Inflate with the builder's context to ensure the correct style is used.
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null);
dialogInflater = LayoutInflater.from(builder.getContext());
selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null);
builder.setView(selectionList);
dialog = builder.create();
dialog.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
trackKeys = new ArrayList<>();
trackTitles =
new ArrayAdapter<>(
builder.getContext(), android.R.layout.simple_list_item_multiple_choice);
representationList = dialogView.findViewById(R.id.representation_list);
representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
representationList.setAdapter(trackTitles);
}
public void prepare() {
parameters = DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS;
downloadHelper.prepare(this);
}
// DownloadHelper.Callback implementation.
@Override
public void onPrepared(DownloadHelper helper) {
for (int i = 0; i < downloadHelper.getPeriodCount(); i++) {
TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i);
for (int j = 0; j < trackGroups.length; j++) {
TrackGroup trackGroup = trackGroups.get(j);
for (int k = 0; k < trackGroup.length; k++) {
trackKeys.add(new TrackKey(i, j, k));
trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k)));
}
}
public void onPrepared(DownloadHelper<?> helper) {
if (helper.getPeriodCount() < 1) {
onPrepareError(downloadHelper, new IOException("Content is empty."));
return;
}
if (!trackKeys.isEmpty()) {
builder.setView(dialogView);
}
builder.create().show();
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();
Log.e(TAG, "Failed to start download", e);
dialog.cancel();
}
// View.OnClickListener implementation.
@Override
public void onClick(View v) {
Integer rendererIndex = (Integer) v.getTag();
String dialogTitle = getTrackTypeString(mappedTrackInfo.getRendererType(rendererIndex));
Pair<AlertDialog, TrackSelectionView> dialogPair =
TrackSelectionView.getDialog(
dialog.getContext(),
dialogTitle,
mappedTrackInfo,
rendererIndex,
parameters,
/* callback= */ this);
dialogPair.second.setShowDisableOption(true);
dialogPair.second.setAllowAdaptiveSelections(false);
dialogPair.first.show();
}
// TrackSelectionView.DialogCallback implementation.
@Override
public void onTracksSelected(DefaultTrackSelector.Parameters parameters) {
for (int i = 0; i < downloadHelper.getPeriodCount(); i++) {
downloadHelper.replaceTrackSelections(/* periodIndex= */ i, parameters);
}
this.parameters = parameters;
updateSelectionList();
}
// DialogInterface.OnClickListener implementation.
@Override
public void onClick(DialogInterface dialog, int which) {
ArrayList<TrackKey> selectedTrackKeys = new ArrayList<>();
for (int i = 0; i < representationList.getChildCount(); i++) {
if (representationList.isItemChecked(i)) {
selectedTrackKeys.add(trackKeys.get(i));
DownloadAction downloadAction = downloadHelper.getDownloadAction(Util.getUtf8Bytes(name));
startDownload(downloadAction);
}
// Internal methods.
private void updateSelectionList() {
selectionList.removeAllViews();
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i);
if (trackGroupArray.length == 0) {
continue;
}
String trackTypeString =
getTrackTypeString(mappedTrackInfo.getRendererType(/* rendererIndex= */ i));
if (trackTypeString == null) {
return;
}
String trackSelectionsString = getTrackSelectionString(/* rendererIndex= */ i);
View view = dialogInflater.inflate(R.layout.download_track_item, selectionList, false);
TextView trackTitleView = view.findViewById(R.id.track_title);
TextView trackDescView = view.findViewById(R.id.track_desc);
ImageButton editButton = view.findViewById(R.id.edit_button);
trackTitleView.setText(trackTypeString);
trackDescView.setText(trackSelectionsString);
editButton.setTag(i);
editButton.setOnClickListener(this);
selectionList.addView(view);
}
}
private String getTrackSelectionString(int rendererIndex) {
List<TrackSelection> trackSelections =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, rendererIndex);
String selectedTracks = "";
Resources resources = selectionList.getResources();
for (int i = 0; i < trackSelections.size(); i++) {
TrackSelection selection = trackSelections.get(i);
for (int j = 0; j < selection.length(); j++) {
String trackName = trackNameProvider.getTrackName(selection.getFormat(j));
if (i == 0 && j == 0) {
selectedTracks = trackName;
} else {
selectedTracks = resources.getString(R.string.exo_item_list, selectedTracks, trackName);
}
}
}
if (!selectedTrackKeys.isEmpty() || trackKeys.isEmpty()) {
// We have selected keys, or we're dealing with single stream content.
DownloadAction downloadAction =
downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys);
startDownload(downloadAction);
return selectedTracks.isEmpty()
? resources.getString(R.string.exo_track_selection_none)
: selectedTracks;
}
@Nullable
private String getTrackTypeString(int trackType) {
Resources resources = selectionList.getResources();
switch (trackType) {
case C.TRACK_TYPE_VIDEO:
return resources.getString(R.string.exo_track_selection_title_video);
case C.TRACK_TYPE_AUDIO:
return resources.getString(R.string.exo_track_selection_title_audio);
case C.TRACK_TYPE_TEXT:
return resources.getString(R.string.exo_track_selection_title_text);
default:
return null;
}
}
}

View File

@ -35,11 +35,11 @@ import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
@ -48,7 +48,6 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
@ -58,11 +57,8 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
@ -416,13 +412,8 @@ public class PlayerActivity extends Activity
boolean preferExtensionDecoders =
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
DefaultRenderersFactory renderersFactory =
new DefaultRenderersFactory(this, extensionRendererMode);
RenderersFactory renderersFactory =
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
trackSelector.setParameters(trackSelectorParameters);
@ -477,21 +468,19 @@ public class PlayerActivity extends Activity
@SuppressWarnings("unchecked")
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
@ContentType int type = Util.inferContentType(uri, overrideExtension);
List<StreamKey> offlineStreamKeys = getOfflineStreamKeys(uri);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(dataSourceFactory)
.setManifestParser(
new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
.setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory)
.setManifestParser(
new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
.setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory)
.setPlaylistParserFactory(
new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri)))
.setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
@ -534,6 +523,9 @@ public class PlayerActivity extends Activity
mediaSource = null;
trackSelector = null;
}
if (adsLoader != null) {
adsLoader.setPlayer(null);
}
releaseMediaDrm();
}
@ -597,6 +589,7 @@ public class PlayerActivity extends Activity
// The demo app has a non-null overlay frame layout.
playerView.getOverlayFrameLayout().addView(adUiViewGroup);
}
adsLoader.setPlayer(player);
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
new AdsMediaSource.MediaSourceFactory() {
@Override

View File

@ -37,6 +37,7 @@ import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
@ -177,7 +178,11 @@ public class SampleChooserActivity extends Activity
.show();
} else {
UriSample uriSample = (UriSample) sample;
downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension);
RenderersFactory renderersFactory =
((DemoApplication) getApplication())
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
downloadTracker.toggleDownload(
this, sample.name, uriSample.uri, uriSample.extension, renderersFactory);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/track_title"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="4dp"/>
<TextView
android:id="@+id/track_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="4dp"/>
</LinearLayout>
<ImageButton
android:id="@+id/edit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:contentDescription="@string/download_edit_track"
android:src="@drawable/ic_edit"/>
</LinearLayout>

View File

@ -13,7 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/representation_list"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/selection_list"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View File

@ -51,6 +51,10 @@
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
<string name="download_edit_track">Edit selection</string>
<string name="download_preparing">Preparing download…</string>
<string name="download_start_error">Failed to start download</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>

View File

@ -31,7 +31,7 @@ android {
}
dependencies {
api 'com.google.android.gms:play-services-cast-framework:16.0.3'
api 'com.google.android.gms:play-services-cast-framework:16.1.2'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
implementation project(modulePrefix + 'library-core')

View File

@ -266,20 +266,29 @@ public final class CastPlayer extends BasePlayer {
// Player implementation.
@Override
@Nullable
public AudioComponent getAudioComponent() {
return null;
}
@Override
@Nullable
public VideoComponent getVideoComponent() {
return null;
}
@Override
@Nullable
public TextComponent getTextComponent() {
return null;
}
@Override
@Nullable
public MetadataComponent getMetadataComponent() {
return null;
}
@Override
public Looper getApplicationLooper() {
return Looper.getMainLooper();

View File

@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
@ -493,6 +494,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
throw new IOException("HTTP request with non-empty body must set Content-Type");
}
if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
requestBuilder.addHeader(
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
}
// Set the Range header.
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
StringBuilder rangeValue = new StringBuilder();

View File

@ -37,6 +37,10 @@ import java.util.List;
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
// Error codes matching ffmpeg_jni.cc.
private static final int DECODER_ERROR_INVALID_DATA = -1;
private static final int DECODER_ERROR_OTHER = -2;
private final String codecName;
private final @Nullable byte[] extraData;
private final @C.Encoding int encoding;
@ -106,8 +110,14 @@ import java.util.List;
int inputSize = inputData.limit();
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
if (result < 0) {
return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result);
if (result == DECODER_ERROR_INVALID_DATA) {
// Treat invalid data errors as non-fatal to match the behavior of MediaCodec. No output will
// be produced for this buffer, so mark it as decode-only to ensure that the audio sink's
// position is reset when more audio is produced.
outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
return null;
} else if (result == DECODER_ERROR_OTHER) {
return new FfmpegDecoderException("Error decoding (see logcat).");
}
if (!hasOutputFormat) {
channelCount = ffmpegGetChannelCount(nativeContext);

View File

@ -63,6 +63,10 @@ static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16;
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
// Error codes matching FfmpegDecoder.java.
static const int DECODER_ERROR_INVALID_DATA = -1;
static const int DECODER_ERROR_OTHER = -2;
/**
* Returns the AVCodec with the specified name, or NULL if it is not available.
*/
@ -79,7 +83,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
/**
* Decodes the packet into the output buffer, returning the number of bytes
* written, or a negative value in the case of an error.
* written, or a negative DECODER_ERROR constant value in the case of an error.
*/
int decodePacket(AVCodecContext *context, AVPacket *packet,
uint8_t *outputBuffer, int outputSize);
@ -238,6 +242,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
context->channels = rawChannelCount;
context->channel_layout = av_get_default_channel_layout(rawChannelCount);
}
context->err_recognition = AV_EF_IGNORE_ERR;
int result = avcodec_open2(context, codec, NULL);
if (result < 0) {
logError("avcodec_open2", result);
@ -254,7 +259,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
result = avcodec_send_packet(context, packet);
if (result) {
logError("avcodec_send_packet", result);
return result;
return result == AVERROR_INVALIDDATA ? DECODER_ERROR_INVALID_DATA
: DECODER_ERROR_OTHER;
}
// Dequeue output data until it runs out.

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

@ -47,7 +47,6 @@ import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
@ -74,7 +73,13 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
/** Loads ads using the IMA SDK. All methods are called on the main thread. */
/**
* {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread.
*
* <p>The player instance that will play the loaded ads must be set before playback using {@link
* #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
* {@link #release()}.
*/
public final class ImaAdsLoader
implements Player.EventListener,
AdsLoader,
@ -93,9 +98,9 @@ public final class ImaAdsLoader
private final Context context;
private @Nullable ImaSdkSettings imaSdkSettings;
private @Nullable AdEventListener adEventListener;
private @Nullable Set<UiElement> adUiElements;
@Nullable private ImaSdkSettings imaSdkSettings;
@Nullable private AdEventListener adEventListener;
@Nullable private Set<UiElement> adUiElements;
private int vastLoadTimeoutMs;
private int mediaLoadTimeoutMs;
private int mediaBitrate;
@ -317,10 +322,11 @@ public final class ImaAdsLoader
private final AdDisplayContainer adDisplayContainer;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
@Nullable private Player nextPlayer;
private Object pendingAdRequestContext;
private List<String> supportedMimeTypes;
private EventListener eventListener;
private Player player;
@Nullable private EventListener eventListener;
@Nullable private Player player;
private VideoProgressUpdate lastContentProgress;
private VideoProgressUpdate lastAdProgress;
private int lastVolumePercentage;
@ -526,6 +532,14 @@ public final class ImaAdsLoader
// AdsLoader implementation.
@Override
public void setPlayer(@Nullable Player player) {
Assertions.checkState(Looper.getMainLooper() == Looper.myLooper());
Assertions.checkState(
player == null || player.getApplicationLooper() == Looper.getMainLooper());
nextPlayer = player;
}
@Override
public void setSupportedContentTypes(@C.ContentType int... contentTypes) {
List<String> supportedMimeTypes = new ArrayList<>();
@ -550,9 +564,10 @@ public final class ImaAdsLoader
}
@Override
public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
Assertions.checkArgument(player.getApplicationLooper() == Looper.getMainLooper());
this.player = player;
public void start(EventListener eventListener, ViewGroup adUiViewGroup) {
Assertions.checkNotNull(
nextPlayer, "Set player using adsLoader.setPlayer before preparing the player.");
player = nextPlayer;
this.eventListener = eventListener;
lastVolumePercentage = 0;
lastAdProgress = null;
@ -576,7 +591,7 @@ public final class ImaAdsLoader
}
@Override
public void detachPlayer() {
public void stop() {
if (adsManager != null && imaPausedContent) {
adPlaybackState =
adPlaybackState.withAdResumePositionUs(
@ -598,6 +613,8 @@ public final class ImaAdsLoader
adsManager.destroy();
adsManager = null;
}
adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this);
adsLoader.removeAdErrorListener(/* adErrorListener= */ this);
imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE;
pendingAdLoadError = null;

View File

@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.ima;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
@ -33,7 +32,8 @@ import java.io.IOException;
/**
* A {@link MediaSource} that inserts ads linearly with a provided content media source.
*
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
* @deprecated Use {@link com.google.android.exoplayer2.source.ads.AdsMediaSource} with
* ImaAdsLoader.
*/
@Deprecated
public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener {
@ -83,12 +83,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
}
@Override
public void prepareSourceInternal(
final ExoPlayer player,
boolean isTopLevelSource,
@Nullable TransferListener mediaTransferListener) {
adsMediaSource.prepareSource(
player, isTopLevelSource, /* listener= */ this, mediaTransferListener);
public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
adsMediaSource.prepareSource(/* listener= */ this, mediaTransferListener);
}
@Override
@ -97,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

@ -64,14 +64,17 @@ import java.util.Set;
};
}
@Override
public int getVastMediaWidth() {
throw new UnsupportedOperationException();
}
@Override
public int getVastMediaHeight() {
throw new UnsupportedOperationException();
}
@Override
public int getVastMediaBitrate() {
throw new UnsupportedOperationException();
}

View File

@ -111,7 +111,7 @@ public class ImaAdsLoaderTest {
@Test
public void testAttachPlayer_setsAdUiViewGroup() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
verify(adDisplayContainer, atLeastOnce()).setAdContainer(adUiViewGroup);
}
@ -119,7 +119,7 @@ public class ImaAdsLoaderTest {
@Test
public void testAttachPlayer_updatesAdPlaybackState() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
@ -131,14 +131,14 @@ public class ImaAdsLoaderTest {
public void testAttachAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
}
@Test
public void testAttachAndCallbacksAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
fakeExoPlayer.setState(Player.STATE_READY, true);
@ -166,7 +166,7 @@ public class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
// Load the preroll ad.
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
imaAdsLoader.loadAd(TEST_URI.toString());
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
@ -210,6 +210,7 @@ public class ImaAdsLoaderTest {
.setImaFactory(testImaFactory)
.setImaSdkSettings(imaSdkSettings)
.buildForAdTag(TEST_URI);
imaAdsLoader.setPlayer(fakeExoPlayer);
}
private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) {

View File

@ -129,7 +129,7 @@ public final class JobDispatcherScheduler implements Scheduler {
Bundle extras = new Bundle();
extras.putString(KEY_SERVICE_ACTION, serviceAction);
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
builder.setExtras(extras);
return builder.build();

View File

@ -67,10 +67,10 @@ import java.util.Map;
*
* <ul>
* <li>Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code
* PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
* when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
* actions can be handled by passing one or more {@link CustomActionProvider}s in a similar
* way.
* PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed to
* {@link #setPlaybackPreparer(PlaybackPreparer)}.
* <li>Custom actions can be handled by passing one or more {@link CustomActionProvider}s to
* {@link #setCustomActionProviders(CustomActionProvider...)}.
* <li>To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by
* calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator}
* is recommended for most use cases.
@ -339,21 +339,21 @@ public final class MediaSessionConnector {
/** The wrapped {@link MediaSessionCompat}. */
public final MediaSessionCompat mediaSession;
@Nullable private final MediaMetadataProvider mediaMetadataProvider;
private final ExoPlayerEventListener exoPlayerEventListener;
private final MediaSessionCallback mediaSessionCallback;
private final Looper looper;
private final ComponentListener componentListener;
private final ArrayList<CommandReceiver> commandReceivers;
private Player player;
private ControlDispatcher controlDispatcher;
private CustomActionProvider[] customActionProviders;
private Map<String, CustomActionProvider> customActionMap;
@Nullable private MediaMetadataProvider mediaMetadataProvider;
@Nullable private Player player;
@Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
@Nullable private Pair<Integer, CharSequence> customError;
private PlaybackPreparer playbackPreparer;
private QueueNavigator queueNavigator;
private QueueEditor queueEditor;
private RatingCallback ratingCallback;
@Nullable private PlaybackPreparer playbackPreparer;
@Nullable private QueueNavigator queueNavigator;
@Nullable private QueueEditor queueEditor;
@Nullable private RatingCallback ratingCallback;
private long enabledPlaybackActions;
private int rewindMs;
@ -362,82 +362,60 @@ public final class MediaSessionConnector {
/**
* Creates an instance.
*
* <p>Equivalent to {@code MediaSessionConnector(mediaSession, new
* DefaultMediaMetadataProvider(mediaSession.getController(), null))}.
*
* @param mediaSession The {@link MediaSessionCompat} to connect to.
*/
public MediaSessionConnector(MediaSessionCompat mediaSession) {
this(
mediaSession,
new DefaultMediaMetadataProvider(mediaSession.getController(), null));
}
/**
* Creates an instance.
*
* @param mediaSession The {@link MediaSessionCompat} to connect to.
* @param mediaMetadataProvider A {@link MediaMetadataProvider} for providing a custom metadata
* object to be published to the media session, or {@code null} if metadata shouldn't be
* published.
*/
public MediaSessionConnector(
MediaSessionCompat mediaSession,
@Nullable MediaMetadataProvider mediaMetadataProvider) {
this.mediaSession = mediaSession;
this.mediaMetadataProvider = mediaMetadataProvider;
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
mediaSessionCallback = new MediaSessionCallback();
exoPlayerEventListener = new ExoPlayerEventListener();
controlDispatcher = new DefaultControlDispatcher();
customActionMap = Collections.emptyMap();
looper = Util.getLooper();
componentListener = new ComponentListener();
commandReceivers = new ArrayList<>();
controlDispatcher = new DefaultControlDispatcher();
customActionProviders = new CustomActionProvider[0];
customActionMap = Collections.emptyMap();
mediaMetadataProvider =
new DefaultMediaMetadataProvider(
mediaSession.getController(), /* metadataExtrasPrefix= */ null);
enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS;
rewindMs = DEFAULT_REWIND_MS;
fastForwardMs = DEFAULT_FAST_FORWARD_MS;
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
mediaSession.setCallback(componentListener, new Handler(looper));
}
/**
* Sets the player to be connected to the media session. Must be called on the same thread that is
* used to access the player.
*
* <p>The order in which any {@link CustomActionProvider}s are passed determines the order of the
* actions published with the playback state of the session.
*
* @param player The player to be connected to the {@code MediaSession}, or {@code null} to
* disconnect the current player.
* @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player.
* @param customActionProviders Optional {@link CustomActionProvider}s to publish and handle
* custom actions.
*/
public void setPlayer(
@Nullable Player player,
@Nullable PlaybackPreparer playbackPreparer,
CustomActionProvider... customActionProviders) {
Assertions.checkArgument(player == null || player.getApplicationLooper() == Looper.myLooper());
public void setPlayer(@Nullable Player player) {
Assertions.checkArgument(player == null || player.getApplicationLooper() == looper);
if (this.player != null) {
this.player.removeListener(exoPlayerEventListener);
mediaSession.setCallback(null);
this.player.removeListener(componentListener);
}
unregisterCommandReceiver(this.playbackPreparer);
this.player = player;
this.playbackPreparer = playbackPreparer;
registerCommandReceiver(playbackPreparer);
this.customActionProviders =
(player != null && customActionProviders != null)
? customActionProviders
: new CustomActionProvider[0];
if (player != null) {
Handler handler = new Handler(Util.getLooper());
mediaSession.setCallback(mediaSessionCallback, handler);
player.addListener(exoPlayerEventListener);
player.addListener(componentListener);
}
invalidateMediaSessionPlaybackState();
invalidateMediaSessionMetadata();
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}.
*/
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
if (this.playbackPreparer != playbackPreparer) {
unregisterCommandReceiver(this.playbackPreparer);
this.playbackPreparer = playbackPreparer;
registerCommandReceiver(playbackPreparer);
invalidateMediaSessionPlaybackState();
}
}
/**
* Sets the {@link ControlDispatcher}.
*
@ -570,6 +548,32 @@ public final class MediaSessionConnector {
invalidateMediaSessionPlaybackState();
}
/**
* Sets custom action providers. The order of the {@link CustomActionProvider}s determines the
* order in which the actions are published.
*
* @param customActionProviders The custom action providers, or null to remove all existing custom
* action providers.
*/
public void setCustomActionProviders(@Nullable CustomActionProvider... customActionProviders) {
this.customActionProviders =
customActionProviders == null ? new CustomActionProvider[0] : customActionProviders;
invalidateMediaSessionPlaybackState();
}
/**
* Sets a provider of metadata to be published to the media session.
*
* @param mediaMetadataProvider The provider of metadata to publish, or {@code null} if no
* metadata should be published.
*/
public void setMediaMetadataProvider(@Nullable MediaMetadataProvider mediaMetadataProvider) {
if (this.mediaMetadataProvider != mediaMetadataProvider) {
this.mediaMetadataProvider = mediaMetadataProvider;
invalidateMediaSessionMetadata();
}
}
/**
* Updates the metadata of the media session.
*
@ -577,9 +581,11 @@ public final class MediaSessionConnector {
* changed and the metadata should be updated immediately.
*/
public final void invalidateMediaSessionMetadata() {
if (mediaMetadataProvider != null && player != null) {
mediaSession.setMetadata(mediaMetadataProvider.getMetadata(player));
}
MediaMetadataCompat metadata =
mediaMetadataProvider != null && player != null
? mediaMetadataProvider.getMetadata(player)
: null;
mediaSession.setMetadata(metadata);
}
/**
@ -591,7 +597,7 @@ public final class MediaSessionConnector {
public final void invalidateMediaSessionPlaybackState() {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
if (player == null) {
builder.setActions(/* capabilities= */ 0).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
mediaSession.setPlaybackState(builder.build());
return;
}
@ -627,7 +633,7 @@ public final class MediaSessionConnector {
Bundle extras = new Bundle();
extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
builder
.setActions(buildPlaybackActions(player))
.setActions(buildPrepareActions() | buildPlaybackActions(player))
.setActiveQueueItemId(activeQueueItemId)
.setBufferedPosition(player.getBufferedPosition())
.setState(
@ -662,6 +668,12 @@ public final class MediaSessionConnector {
commandReceivers.remove(commandReceiver);
}
private long buildPrepareActions() {
return playbackPreparer == null
? 0
: (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
}
private long buildPlaybackActions(Player player) {
boolean enableSeeking = false;
boolean enableRewind = false;
@ -688,9 +700,6 @@ public final class MediaSessionConnector {
playbackActions &= enabledPlaybackActions;
long actions = playbackActions;
if (playbackPreparer != null) {
actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
}
if (queueNavigator != null) {
actions |=
(QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
@ -719,8 +728,7 @@ public final class MediaSessionConnector {
}
private boolean canDispatchToPlaybackPreparer(long action) {
return player != null
&& playbackPreparer != null
return playbackPreparer != null
&& (playbackPreparer.getSupportedPrepareActions() & action) != 0;
}
@ -738,6 +746,13 @@ public final class MediaSessionConnector {
return player != null && queueEditor != null;
}
private void stopPlayerForPrepare(boolean playWhenReady) {
if (player != null) {
player.stop();
player.setPlayWhenReady(playWhenReady);
}
}
private void rewind(Player player) {
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
seekTo(player, player.getCurrentPosition() - rewindMs);
@ -865,11 +880,14 @@ public final class MediaSessionConnector {
}
}
private class ExoPlayerEventListener implements Player.EventListener {
private class ComponentListener extends MediaSessionCompat.Callback
implements Player.EventListener {
private int currentWindowIndex;
private int currentWindowCount;
// Player.EventListener implementation.
@Override
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
@ -932,9 +950,8 @@ public final class MediaSessionConnector {
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
invalidateMediaSessionPlaybackState();
}
}
private class MediaSessionCallback extends MediaSessionCompat.Callback {
// MediaSessionCompat.Callback implementation.
@Override
public void onPlay() {
@ -1058,8 +1075,7 @@ public final class MediaSessionConnector {
@Override
public void onPrepare() {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
player.stop();
player.setPlayWhenReady(false);
stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepare();
}
}
@ -1067,8 +1083,7 @@ public final class MediaSessionConnector {
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
player.stop();
player.setPlayWhenReady(false);
stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
}
}
@ -1076,8 +1091,7 @@ public final class MediaSessionConnector {
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
player.stop();
player.setPlayWhenReady(false);
stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromSearch(query, extras);
}
}
@ -1085,8 +1099,7 @@ public final class MediaSessionConnector {
@Override
public void onPrepareFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
player.stop();
player.setPlayWhenReady(false);
stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromUri(uri, extras);
}
}
@ -1094,8 +1107,7 @@ public final class MediaSessionConnector {
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
player.stop();
player.setPlayWhenReady(true);
stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
}
}
@ -1103,8 +1115,7 @@ public final class MediaSessionConnector {
@Override
public void onPlayFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
player.stop();
player.setPlayWhenReady(true);
stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromSearch(query, extras);
}
}
@ -1112,8 +1123,7 @@ public final class MediaSessionConnector {
@Override
public void onPlayFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
player.stop();
player.setPlayWhenReady(true);
stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromUri(uri, extras);
}
}

View File

@ -22,9 +22,7 @@ import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.util.RepeatModeUtil;
/**
* Provides a custom action for toggling repeat modes.
*/
/** Provides a custom action for toggling repeat modes. */
public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
/** The default repeat toggle modes. */

View File

@ -65,13 +65,6 @@ public final class TimelineQueueEditor
* {@link MediaSessionConnector}.
*/
public interface QueueDataAdapter {
/**
* Gets the {@link MediaDescriptionCompat} for a {@code position}.
*
* @param position The position in the queue for which to provide a description.
* @return A {@link MediaDescriptionCompat}.
*/
MediaDescriptionCompat getMediaDescription(int position);
/**
* Adds a {@link MediaDescriptionCompat} at the given {@code position}.
*

View File

@ -41,7 +41,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
private final MediaSessionCompat mediaSession;
private final Timeline.Window window;
protected final int maxQueueSize;
private final int maxQueueSize;
private long activeQueueItemId;

View File

@ -21,6 +21,7 @@ import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
@ -263,7 +264,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
long position = dataSpec.position;
long length = dataSpec.length;
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
if (url == null) {
@ -293,10 +293,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
if (userAgent != null) {
builder.addHeader("User-Agent", userAgent);
}
if (!allowGzip) {
if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
builder.addHeader("Accept-Encoding", "identity");
}
if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
builder.addHeader(
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
}
RequestBody requestBody = null;
if (dataSpec.httpBody != null) {
requestBody = RequestBody.create(null, dataSpec.httpBody);

View File

@ -39,7 +39,7 @@ either instantiated and injected from application code, or obtained from
instances of `DataSource.Factory` that are instantiated and injected from
application code.
`DefaultDataSource` will automatically use uses the RTMP extension whenever it's
`DefaultDataSource` will automatically use the RTMP extension whenever it's
available. Hence if your application is using `DefaultDataSource` or
`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as
adding a dependency to the RTMP extension as described above. No changes to your

View File

@ -127,8 +127,8 @@ 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;
@ -364,24 +364,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
clearReportedVideoSize();
clearRenderedFirstFrame();
try {
setSourceDrmSession(null);
releaseDecoder();
} finally {
try {
if (drmSession != null) {
drmSessionManager.releaseSession(drmSession);
}
} finally {
try {
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} finally {
drmSession = null;
pendingDrmSession = null;
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters);
}
}
eventDispatcher.disabled(decoderCounters);
}
}
@ -433,18 +419,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 +470,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;
@ -704,12 +711,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.
@ -922,12 +930,12 @@ 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;
}

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. */
@ -896,6 +899,26 @@ public final class C {
*/
public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
/** Video projection types. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
Format.NO_VALUE,
PROJECTION_RECTANGULAR,
PROJECTION_EQUIRECTANGULAR,
PROJECTION_CUBEMAP,
PROJECTION_MESH
})
public @interface Projection {}
/** Conventional rectangular projection. */
public static final int PROJECTION_RECTANGULAR = 0;
/** Equirectangular spherical projection. */
public static final int PROJECTION_EQUIRECTANGULAR = 1;
/** Cube map projection. */
public static final int PROJECTION_CUBEMAP = 2;
/** 3-D mesh projection. */
public static final int PROJECTION_MESH = 3;
/**
* Priority for media playback.
*

View File

@ -139,26 +139,34 @@ import java.util.concurrent.CopyOnWriteArrayList;
repeatMode,
shuffleModeEnabled,
eventHandler,
this,
clock);
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
}
@Override
@Nullable
public AudioComponent getAudioComponent() {
return null;
}
@Override
@Nullable
public VideoComponent getVideoComponent() {
return null;
}
@Override
@Nullable
public TextComponent getTextComponent() {
return null;
}
@Override
@Nullable
public MetadataComponent getMetadataComponent() {
return null;
}
@Override
public Looper getPlaybackLooper() {
return internalPlayer.getPlaybackLooper();

View File

@ -95,7 +95,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
private final HandlerWrapper handler;
private final HandlerThread internalPlaybackThread;
private final Handler eventHandler;
private final ExoPlayer player;
private final Timeline.Window window;
private final Timeline.Period period;
private final long backBufferDurationUs;
@ -134,7 +133,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled,
Handler eventHandler,
ExoPlayer player,
Clock clock) {
this.renderers = renderers;
this.trackSelector = trackSelector;
@ -145,7 +143,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
this.repeatMode = repeatMode;
this.shuffleModeEnabled = shuffleModeEnabled;
this.eventHandler = eventHandler;
this.player = player;
this.clock = clock;
this.queue = new MediaPeriodQueue();
@ -441,11 +438,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
loadControl.onPrepared();
this.mediaSource = mediaSource;
setState(Player.STATE_BUFFERING);
mediaSource.prepareSource(
player,
/* isTopLevelSource= */ true,
/* listener= */ this,
bandwidthMeter.getTransferListener());
mediaSource.prepareSource(/* listener= */ this, bandwidthMeter.getTransferListener());
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}

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.2";
public static final String VERSION = "2.9.4";
/** 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.2";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.4";
/**
* 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 = 2009002;
public static final int VERSION_INT = 2009004;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View File

@ -1181,6 +1181,37 @@ public final class Format implements Parcelable {
metadata);
}
public Format copyWithFrameRate(float frameRate) {
return new Format(
id,
label,
containerMimeType,
sampleMimeType,
codecs,
bitrate,
maxInputSize,
width,
height,
frameRate,
rotationDegrees,
pixelWidthHeightRatio,
projectionData,
stereoMode,
colorInfo,
channelCount,
sampleRate,
pcmEncoding,
encoderDelay,
encoderPadding,
selectionFlags,
language,
accessibilityChannel,
subsampleOffsetUs,
initializationData,
drmInitData,
metadata);
}
public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {
return new Format(
id,
@ -1274,6 +1305,37 @@ public final class Format implements Parcelable {
metadata);
}
public Format copyWithBitrate(int bitrate) {
return new Format(
id,
label,
containerMimeType,
sampleMimeType,
codecs,
bitrate,
maxInputSize,
width,
height,
frameRate,
rotationDegrees,
pixelWidthHeightRatio,
projectionData,
stereoMode,
colorInfo,
channelCount,
sampleRate,
pcmEncoding,
encoderDelay,
encoderPadding,
selectionFlags,
language,
accessibilityChannel,
subsampleOffsetUs,
initializationData,
drmInitData,
metadata);
}
/**
* Returns the number of pixels if this is a video format whose {@link #width} and {@link #height}
* are known, or {@link #NO_VALUE} otherwise

View File

@ -89,7 +89,7 @@ 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);
}
/**
@ -399,8 +399,8 @@ 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);
MediaPeriodId id, MediaSource mediaSource, Allocator allocator, long startPositionUs) {
MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs);
if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) {
mediaPeriod =
new ClippingMediaPeriod(

View File

@ -26,6 +26,7 @@ import com.google.android.exoplayer2.C.VideoScalingMode;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioListener;
import com.google.android.exoplayer2.audio.AuxEffectInfo;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@ -299,6 +300,24 @@ public interface Player {
void removeTextOutput(TextOutput listener);
}
/** The metadata component of a {@link Player}. */
interface MetadataComponent {
/**
* Adds a {@link MetadataOutput} to receive metadata.
*
* @param output The output to register.
*/
void addMetadataOutput(MetadataOutput output);
/**
* Removes a {@link MetadataOutput}.
*
* @param output The output to remove.
*/
void removeMetadataOutput(MetadataOutput output);
}
/**
* Listener of changes in player state. All methods have no-op default implementations to allow
* selective overrides.
@ -533,6 +552,12 @@ public interface Player {
@Nullable
TextComponent getTextComponent();
/**
* Returns the component of this player for metadata output, or null if metadata is not supported.
*/
@Nullable
MetadataComponent getMetadataComponent();
/**
* Returns the {@link Looper} associated with the application thread that's used to access the
* player and on which player events are received.

View File

@ -65,7 +65,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
*/
@TargetApi(16)
public class SimpleExoPlayer extends BasePlayer
implements ExoPlayer, Player.AudioComponent, Player.VideoComponent, Player.TextComponent {
implements ExoPlayer,
Player.AudioComponent,
Player.VideoComponent,
Player.TextComponent,
Player.MetadataComponent {
/** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */
@Deprecated
@ -90,25 +94,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;
/**
@ -243,20 +247,29 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
@Nullable
public AudioComponent getAudioComponent() {
return this;
}
@Override
@Nullable
public VideoComponent getVideoComponent() {
return this;
}
@Override
@Nullable
public TextComponent getTextComponent() {
return this;
}
@Override
@Nullable
public MetadataComponent getMetadataComponent() {
return this;
}
/**
* Sets the video scaling mode.
*
@ -545,30 +558,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;
}
@ -713,20 +722,12 @@ public class SimpleExoPlayer extends BasePlayer
removeTextOutput(output);
}
/**
* Adds a {@link MetadataOutput} to receive metadata.
*
* @param listener The output to register.
*/
@Override
public void addMetadataOutput(MetadataOutput listener) {
metadataOutputs.add(listener);
}
/**
* Removes a {@link MetadataOutput}.
*
* @param listener The output to remove.
*/
@Override
public void removeMetadataOutput(MetadataOutput listener) {
metadataOutputs.remove(listener);
}
@ -1048,7 +1049,8 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
public @Nullable Object getCurrentManifest() {
@Nullable
public Object getCurrentManifest() {
verifyApplicationThread();
return player.getCurrentManifest();
}

View File

@ -488,7 +488,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

@ -548,7 +548,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;
@ -366,7 +366,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
if (outputBuffer == null) {
return false;
}
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
if (outputBuffer.skippedOutputBufferCount > 0) {
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
audioSink.handleDiscontinuity();
}
}
if (outputBuffer.isEndOfStream()) {
@ -459,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;
}
@ -565,25 +568,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
audioTrackNeedsConfigure = true;
waitingForKeys = false;
try {
setSourceDrmSession(null);
releaseDecoder();
audioSink.reset();
} finally {
try {
if (drmSession != null) {
drmSessionManager.releaseSession(drmSession);
}
} finally {
try {
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} finally {
drmSession = null;
pendingDrmSession = null;
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters);
}
}
eventDispatcher.disabled(decoderCounters);
}
}
@ -612,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.
@ -643,17 +633,34 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
private void releaseDecoder() {
if (decoder == null) {
return;
}
inputBuffer = null;
outputBuffer = null;
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false;
if (decoder != null) {
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
}
setDecoderDrmSession(null);
}
private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
DrmSession<ExoMediaCrypto> previous = sourceDrmSession;
sourceDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
DrmSession<ExoMediaCrypto> previous = decoderDrmSession;
decoderDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void releaseDrmSessionIfUnused(@Nullable DrmSession<ExoMediaCrypto> session) {
if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
drmSessionManager.releaseSession(session);
}
}
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
@ -668,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,89 @@
/*
* 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.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.google.android.exoplayer2.util.Log;
/**
* 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";
public ExoDatabaseProvider(Context context) {
super(context.getApplicationContext(), DATABASE_NAME, /* 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);
}
}
}
}
}
}

View File

@ -0,0 +1,116 @@
/*
* 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 java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A table that holds version information about other ExoPlayer tables. This allows ExoPlayer tables
* to be versioned independently to the version of the containing database.
*/
public final class VersionTable {
/** Returned by {@link #getVersion(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 functionality. */
public static final int FEATURE_CACHE = 1;
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})
private @interface Feature {}
private final DatabaseProvider databaseProvider;
public VersionTable(DatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider;
// Check whether the table exists to avoid getting a writable database if we don't need one.
if (!doesTableExist(databaseProvider, TABLE_NAME)) {
databaseProvider.getWritableDatabase().execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS);
}
}
/**
* Sets the version of tables belonging to the specified feature.
*
* @param feature The feature.
* @param version The version.
*/
public void setVersion(@Feature int feature, int version) {
ContentValues values = new ContentValues();
values.put(COLUMN_FEATURE, feature);
values.put(COLUMN_VERSION, version);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
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.
*/
public int getVersion(@Feature int feature) {
String selection = COLUMN_FEATURE + " = ?";
String[] selectionArgs = {Integer.toString(feature)};
try (Cursor cursor =
databaseProvider
.getReadableDatabase()
.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);
}
}
/* package */ static boolean doesTableExist(DatabaseProvider databaseProvider, String tableName) {
SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
long count =
DatabaseUtils.queryNumEntries(
readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName});
return count > 0;
}
}

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

@ -17,48 +17,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

@ -34,16 +34,26 @@ public final class MpegAudioHeader {
private static final String[] MIME_TYPE_BY_LAYER =
new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG};
private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000};
private static final int[] BITRATE_V1_L1 =
{32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448};
private static final int[] BITRATE_V2_L1 =
{32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256};
private static final int[] BITRATE_V1_L2 =
{32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384};
private static final int[] BITRATE_V1_L3 =
{32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320};
private static final int[] BITRATE_V2 =
{8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160};
private static final int[] BITRATE_V1_L1 = {
32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000,
416000, 448000
};
private static final int[] BITRATE_V2_L1 = {
32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000,
224000, 256000
};
private static final int[] BITRATE_V1_L2 = {
32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000,
320000, 384000
};
private static final int[] BITRATE_V1_L3 = {
32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000,
320000
};
private static final int[] BITRATE_V2 = {
8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000,
160000
};
/**
* Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it
@ -89,7 +99,7 @@ public final class MpegAudioHeader {
if (layer == 3) {
// Layer I (layer == 3)
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
return (12000 * bitrate / samplingRate + padding) * 4;
return (12 * bitrate / samplingRate + padding) * 4;
} else {
// Layer II (layer == 2) or III (layer == 1)
if (version == 3) {
@ -102,10 +112,10 @@ public final class MpegAudioHeader {
if (version == 3) {
// Version 1
return 144000 * bitrate / samplingRate + padding;
return 144 * bitrate / samplingRate + padding;
} else {
// Version 2 or 2.5
return (layer == 1 ? 72000 : 144000) * bitrate / samplingRate + padding;
return (layer == 1 ? 72 : 144) * bitrate / samplingRate + padding;
}
}
@ -159,7 +169,7 @@ public final class MpegAudioHeader {
if (layer == 3) {
// Layer I (layer == 3)
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
frameSize = (12000 * bitrate / sampleRate + padding) * 4;
frameSize = (12 * bitrate / sampleRate + padding) * 4;
samplesPerFrame = 384;
} else {
// Layer II (layer == 2) or III (layer == 1)
@ -167,19 +177,22 @@ public final class MpegAudioHeader {
// Version 1
bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
samplesPerFrame = 1152;
frameSize = 144000 * bitrate / sampleRate + padding;
frameSize = 144 * bitrate / sampleRate + padding;
} else {
// Version 2 or 2.5.
bitrate = BITRATE_V2[bitrateIndex - 1];
samplesPerFrame = layer == 1 ? 576 : 1152;
frameSize = (layer == 1 ? 72000 : 144000) * bitrate / sampleRate + padding;
frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding;
}
}
// Calculate the bitrate in the same way Mp3Extractor calculates sample timestamps so that
// seeking to a given timestamp and playing from the start up to that timestamp give the same
// results for CBR streams. See also [internal: b/120390268].
bitrate = 8 * frameSize * sampleRate / samplesPerFrame;
String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000,
samplesPerFrame);
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame);
return true;
}
@ -198,8 +211,14 @@ public final class MpegAudioHeader {
/** Number of samples stored in the frame. */
public int samplesPerFrame;
private void setValues(int version, String mimeType, int frameSize, int sampleRate, int channels,
int bitrate, int samplesPerFrame) {
private void setValues(
int version,
String mimeType,
int frameSize,
int sampleRate,
int channels,
int bitrate,
int samplesPerFrame) {
this.version = version;
this.mimeType = mimeType;
this.frameSize = frameSize;

View File

@ -191,7 +191,11 @@ public final class MatroskaExtractor implements Extractor {
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
private static final int ID_LANGUAGE = 0x22B59C;
private static final int ID_PROJECTION = 0x7670;
private static final int ID_PROJECTION_TYPE = 0x7671;
private static final int ID_PROJECTION_PRIVATE = 0x7672;
private static final int ID_PROJECTION_POSE_YAW = 0x7673;
private static final int ID_PROJECTION_POSE_PITCH = 0x7674;
private static final int ID_PROJECTION_POSE_ROLL = 0x7675;
private static final int ID_STEREO_MODE = 0x53B8;
private static final int ID_COLOUR = 0x55B0;
private static final int ID_COLOUR_RANGE = 0x55B9;
@ -760,6 +764,24 @@ public final class MatroskaExtractor implements Extractor {
case ID_MAX_FALL:
currentTrack.maxFrameAverageLuminance = (int) value;
break;
case ID_PROJECTION_TYPE:
switch ((int) value) {
case 0:
currentTrack.projectionType = C.PROJECTION_RECTANGULAR;
break;
case 1:
currentTrack.projectionType = C.PROJECTION_EQUIRECTANGULAR;
break;
case 2:
currentTrack.projectionType = C.PROJECTION_CUBEMAP;
break;
case 3:
currentTrack.projectionType = C.PROJECTION_MESH;
break;
default:
break;
}
break;
default:
break;
}
@ -803,6 +825,15 @@ public final class MatroskaExtractor implements Extractor {
case ID_LUMNINANCE_MIN:
currentTrack.minMasteringLuminance = (float) value;
break;
case ID_PROJECTION_POSE_YAW:
currentTrack.projectionPoseYaw = (float) value;
break;
case ID_PROJECTION_POSE_PITCH:
currentTrack.projectionPosePitch = (float) value;
break;
case ID_PROJECTION_POSE_ROLL:
currentTrack.projectionPoseRoll = (float) value;
break;
default:
break;
}
@ -1465,6 +1496,7 @@ public final class MatroskaExtractor implements Extractor {
case ID_COLOUR_PRIMARIES:
case ID_MAX_CLL:
case ID_MAX_FALL:
case ID_PROJECTION_TYPE:
return TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
case ID_NAME:
@ -1491,6 +1523,9 @@ public final class MatroskaExtractor implements Extractor {
case ID_WHITE_POINT_CHROMATICITY_Y:
case ID_LUMNINANCE_MAX:
case ID_LUMNINANCE_MIN:
case ID_PROJECTION_POSE_YAW:
case ID_PROJECTION_POSE_PITCH:
case ID_PROJECTION_POSE_ROLL:
return TYPE_FLOAT;
default:
return TYPE_UNKNOWN;
@ -1631,6 +1666,10 @@ public final class MatroskaExtractor implements Extractor {
public int displayWidth = Format.NO_VALUE;
public int displayHeight = Format.NO_VALUE;
public int displayUnit = DISPLAY_UNIT_PIXELS;
@C.Projection public int projectionType = Format.NO_VALUE;
public float projectionPoseYaw = 0f;
public float projectionPosePitch = 0f;
public float projectionPoseRoll = 0f;
public byte[] projectionData = null;
@C.StereoMode
public int stereoMode = Format.NO_VALUE;
@ -1850,6 +1889,21 @@ public final class MatroskaExtractor implements Extractor {
} else if ("htc_video_rotA-270".equals(name)) {
rotationDegrees = 270;
}
if (projectionType == C.PROJECTION_RECTANGULAR
&& Float.compare(projectionPoseYaw, 0f) == 0
&& Float.compare(projectionPosePitch, 0f) == 0) {
// The range of projectionPoseRoll is [-180, 180].
if (Float.compare(projectionPoseRoll, 0f) == 0) {
rotationDegrees = 0;
} else if (Float.compare(projectionPosePitch, 90f) == 0) {
rotationDegrees = 90;
} else if (Float.compare(projectionPosePitch, -180f) == 0
|| Float.compare(projectionPosePitch, 180f) == 0) {
rotationDegrees = 180;
} else if (Float.compare(projectionPosePitch, -90f) == 0) {
rotationDegrees = 270;
}
}
format =
Format.createVideoSampleFormat(
Integer.toString(trackId),

View File

@ -22,7 +22,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@SuppressWarnings("ConstantField")
@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"})
/* package */ abstract class Atom {
/**
@ -130,6 +130,7 @@ import java.util.List;
public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb");
public static final int TYPE_udta = Util.getIntegerCodeForString("udta");
public static final int TYPE_meta = Util.getIntegerCodeForString("meta");
public static final int TYPE_keys = Util.getIntegerCodeForString("keys");
public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst");
public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
public static final int TYPE_name = Util.getIntegerCodeForString("name");

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.mp4;
import static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType;
import android.support.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@ -39,7 +40,7 @@ import java.util.Collections;
import java.util.List;
/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */
@SuppressWarnings("ConstantField")
@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"})
/* package */ final class AtomParsers {
private static final String TAG = "AtomParsers";
@ -51,6 +52,7 @@ import java.util.List;
private static final int TYPE_subt = Util.getIntegerCodeForString("subt");
private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
private static final int TYPE_meta = Util.getIntegerCodeForString("meta");
private static final int TYPE_mdta = Util.getIntegerCodeForString("mdta");
/**
* The threshold number of samples to trim from the start/end of an audio track when applying an
@ -77,7 +79,7 @@ import java.util.List;
DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime)
throws ParserException {
Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data));
if (trackType == C.TRACK_TYPE_UNKNOWN) {
return null;
}
@ -485,6 +487,7 @@ import java.util.List;
* @param isQuickTime True for QuickTime media. False otherwise.
* @return Parsed metadata, or null.
*/
@Nullable
public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
if (isQuickTime) {
// Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
@ -499,14 +502,69 @@ import java.util.List;
int atomType = udtaData.readInt();
if (atomType == Atom.TYPE_meta) {
udtaData.setPosition(atomPosition);
return parseMetaAtom(udtaData, atomPosition + atomSize);
return parseUdtaMeta(udtaData, atomPosition + atomSize);
}
udtaData.skipBytes(atomSize - Atom.HEADER_SIZE);
udtaData.setPosition(atomPosition + atomSize);
}
return null;
}
private static Metadata parseMetaAtom(ParsableByteArray meta, int limit) {
/**
* Parses a metadata meta atom if it contains metadata with handler 'mdta'.
*
* @param meta The metadata atom to decode.
* @return Parsed metadata, or null.
*/
@Nullable
public static Metadata parseMdtaFromMeta(Atom.ContainerAtom meta) {
Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr);
Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys);
Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst);
if (hdlrAtom == null
|| keysAtom == null
|| ilstAtom == null
|| AtomParsers.parseHdlr(hdlrAtom.data) != TYPE_mdta) {
// There isn't enough information to parse the metadata, or the handler type is unexpected.
return null;
}
// Parse metadata keys.
ParsableByteArray keys = keysAtom.data;
keys.setPosition(Atom.FULL_HEADER_SIZE);
int entryCount = keys.readInt();
String[] keyNames = new String[entryCount];
for (int i = 0; i < entryCount; i++) {
int entrySize = keys.readInt();
keys.skipBytes(4); // keyNamespace
int keySize = entrySize - 8;
keyNames[i] = keys.readString(keySize);
}
// Parse metadata items.
ParsableByteArray ilst = ilstAtom.data;
ilst.setPosition(Atom.HEADER_SIZE);
ArrayList<Metadata.Entry> entries = new ArrayList<>();
while (ilst.bytesLeft() > Atom.HEADER_SIZE) {
int atomPosition = ilst.getPosition();
int atomSize = ilst.readInt();
int keyIndex = ilst.readInt() - 1;
if (keyIndex >= 0 && keyIndex < keyNames.length) {
String key = keyNames[keyIndex];
Metadata.Entry entry =
MetadataUtil.parseMdtaMetadataEntryFromIlst(ilst, atomPosition + atomSize, key);
if (entry != null) {
entries.add(entry);
}
} else {
Log.w(TAG, "Skipped metadata with unknown key index: " + keyIndex);
}
ilst.setPosition(atomPosition + atomSize);
}
return entries.isEmpty() ? null : new Metadata(entries);
}
@Nullable
private static Metadata parseUdtaMeta(ParsableByteArray meta, int limit) {
meta.skipBytes(Atom.FULL_HEADER_SIZE);
while (meta.getPosition() < limit) {
int atomPosition = meta.getPosition();
@ -516,11 +574,12 @@ import java.util.List;
meta.setPosition(atomPosition);
return parseIlst(meta, atomPosition + atomSize);
}
meta.skipBytes(atomSize - Atom.HEADER_SIZE);
meta.setPosition(atomPosition + atomSize);
}
return null;
}
@Nullable
private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
ilst.skipBytes(Atom.HEADER_SIZE);
ArrayList<Metadata.Entry> entries = new ArrayList<>();
@ -610,19 +669,22 @@ import java.util.List;
* Parses an hdlr atom.
*
* @param hdlr The hdlr atom to decode.
* @return The track type.
* @return The handler value.
*/
private static int parseHdlr(ParsableByteArray hdlr) {
hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
int trackType = hdlr.readInt();
if (trackType == TYPE_soun) {
return hdlr.readInt();
}
/** Returns the track type for a given handler value. */
private static int getTrackTypeForHdlr(int hdlr) {
if (hdlr == TYPE_soun) {
return C.TRACK_TYPE_AUDIO;
} else if (trackType == TYPE_vide) {
} else if (hdlr == TYPE_vide) {
return C.TRACK_TYPE_VIDEO;
} else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt
|| trackType == TYPE_clcp) {
} else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) {
return C.TRACK_TYPE_TEXT;
} else if (trackType == TYPE_meta) {
} else if (hdlr == TYPE_meta) {
return C.TRACK_TYPE_METADATA;
} else {
return C.TRACK_TYPE_UNKNOWN;

View File

@ -0,0 +1,115 @@
/*
* 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.extractor.mp4;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
/**
* Stores extensible metadata with handler type 'mdta'. See also the QuickTime File Format
* Specification.
*/
public final class MdtaMetadataEntry implements Metadata.Entry {
/** The metadata key name. */
public final String key;
/** The payload. The interpretation of the value depends on {@link #typeIndicator}. */
public final byte[] value;
/** The four byte locale indicator. */
public final int localeIndicator;
/** The four byte type indicator. */
public final int typeIndicator;
/** Creates a new metadata entry for the specified metadata key/value. */
public MdtaMetadataEntry(String key, byte[] value, int localeIndicator, int typeIndicator) {
this.key = key;
this.value = value;
this.localeIndicator = localeIndicator;
this.typeIndicator = typeIndicator;
}
private MdtaMetadataEntry(Parcel in) {
key = Util.castNonNull(in.readString());
value = new byte[in.readInt()];
in.readByteArray(value);
localeIndicator = in.readInt();
typeIndicator = in.readInt();
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
MdtaMetadataEntry other = (MdtaMetadataEntry) obj;
return key.equals(other.key)
&& Arrays.equals(value, other.value)
&& localeIndicator == other.localeIndicator
&& typeIndicator == other.typeIndicator;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + key.hashCode();
result = 31 * result + Arrays.hashCode(value);
result = 31 * result + localeIndicator;
result = 31 * result + typeIndicator;
return result;
}
@Override
public String toString() {
return "mdta: key=" + key;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(key);
dest.writeInt(value.length);
dest.writeByteArray(value);
dest.writeInt(localeIndicator);
dest.writeInt(typeIndicator);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<MdtaMetadataEntry> CREATOR =
new Parcelable.Creator<MdtaMetadataEntry>() {
@Override
public MdtaMetadataEntry createFromParcel(Parcel in) {
return new MdtaMetadataEntry(in);
}
@Override
public MdtaMetadataEntry[] newArray(int size) {
return new MdtaMetadataEntry[size];
}
};
}

View File

@ -16,6 +16,9 @@
package com.google.android.exoplayer2.extractor.mp4;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
@ -25,10 +28,9 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
/**
* Parses metadata items stored in ilst atoms.
*/
/** Utilities for handling metadata in MP4. */
/* package */ final class MetadataUtil {
private static final String TAG = "MetadataUtil";
@ -103,24 +105,73 @@ import com.google.android.exoplayer2.util.Util;
private static final String LANGUAGE_UNDEFINED = "und";
private static final int TYPE_TOP_BYTE_COPYRIGHT = 0xA9;
private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD.
private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps";
private static final int MDTA_TYPE_INDICATOR_FLOAT = 23;
private MetadataUtil() {}
/**
* Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting
* from the current position of the {@link ParsableByteArray}, and the position is advanced by the
* size of the element. The position is advanced even if the element's type is unrecognized.
* Returns a {@link Format} that is the same as the input format but includes information from the
* specified sources of metadata.
*/
public static Format getFormatWithMetadata(
int trackType,
Format format,
@Nullable Metadata udtaMetadata,
@Nullable Metadata mdtaMetadata,
GaplessInfoHolder gaplessInfoHolder) {
if (trackType == C.TRACK_TYPE_AUDIO) {
if (gaplessInfoHolder.hasGaplessInfo()) {
format =
format.copyWithGaplessInfo(
gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding);
}
// We assume all udta metadata is associated with the audio track.
if (udtaMetadata != null) {
format = format.copyWithMetadata(udtaMetadata);
}
} else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) {
// Populate only metadata keys that are known to be specific to video.
for (int i = 0; i < mdtaMetadata.length(); i++) {
Metadata.Entry entry = mdtaMetadata.get(i);
if (entry instanceof MdtaMetadataEntry) {
MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry;
if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key)
&& mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) {
try {
float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get();
format = format.copyWithFrameRate(fps);
format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry));
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring invalid framerate");
}
}
}
}
}
return format;
}
/**
* Parses a single userdata ilst element from a {@link ParsableByteArray}. The element is read
* starting from the current position of the {@link ParsableByteArray}, and the position is
* advanced by the size of the element. The position is advanced even if the element's type is
* unrecognized.
*
* @param ilst Holds the data to be parsed.
* @return The parsed element, or null if the element's type was not recognized.
*/
public static @Nullable Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
@Nullable
public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
int position = ilst.getPosition();
int endPosition = position + ilst.readInt();
int type = ilst.readInt();
int typeTopByte = (type >> 24) & 0xFF;
try {
if (typeTopByte == '\u00A9' /* Copyright char */
|| typeTopByte == '\uFFFD' /* Replacement char */) {
if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) {
int shortType = type & 0x00FFFFFF;
if (shortType == SHORT_TYPE_COMMENT) {
return parseCommentAttribute(type, ilst);
@ -185,7 +236,36 @@ import com.google.android.exoplayer2.util.Util;
}
}
private static @Nullable TextInformationFrame parseTextAttribute(
/**
* Parses an 'mdta' metadata entry starting at the current position in an ilst box.
*
* @param ilst The ilst box.
* @param endPosition The end position of the entry in the ilst box.
* @param key The mdta metadata entry key for the entry.
* @return The parsed element, or null if the entry wasn't recognized.
*/
@Nullable
public static MdtaMetadataEntry parseMdtaMetadataEntryFromIlst(
ParsableByteArray ilst, int endPosition, String key) {
int atomPosition;
while ((atomPosition = ilst.getPosition()) < endPosition) {
int atomSize = ilst.readInt();
int atomType = ilst.readInt();
if (atomType == Atom.TYPE_data) {
int typeIndicator = ilst.readInt();
int localeIndicator = ilst.readInt();
int dataSize = atomSize - 16;
byte[] value = new byte[dataSize];
ilst.readBytes(value, 0, dataSize);
return new MdtaMetadataEntry(key, value, localeIndicator, typeIndicator);
}
ilst.setPosition(atomPosition + atomSize);
}
return null;
}
@Nullable
private static TextInformationFrame parseTextAttribute(
int type, String id, ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
@ -198,7 +278,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
private static @Nullable CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
@Nullable
private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data) {
@ -210,7 +291,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
private static @Nullable Id3Frame parseUint8Attribute(
@Nullable
private static Id3Frame parseUint8Attribute(
int type,
String id,
ParsableByteArray data,
@ -229,7 +311,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
private static @Nullable TextInformationFrame parseIndexAndCountAttribute(
@Nullable
private static TextInformationFrame parseIndexAndCountAttribute(
int type, String attributeName, ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
@ -249,8 +332,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
private static @Nullable TextInformationFrame parseStandardGenreAttribute(
ParsableByteArray data) {
@Nullable
private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {
int genreCode = parseUint8AttributeValue(data);
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
? STANDARD_GENRES[genreCode - 1] : null;
@ -261,7 +344,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
private static @Nullable ApicFrame parseCoverArt(ParsableByteArray data) {
@Nullable
private static ApicFrame parseCoverArt(ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data) {
@ -285,8 +369,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
private static @Nullable Id3Frame parseInternalAttribute(
ParsableByteArray data, int endPosition) {
@Nullable
private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) {
String domain = null;
String name = null;
int dataAtomPosition = -1;

View File

@ -75,7 +75,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private static final int STATE_READING_ATOM_PAYLOAD = 1;
private static final int STATE_READING_SAMPLE = 2;
// Brand stored in the ftyp atom for QuickTime media.
/** Brand stored in the ftyp atom for QuickTime media. */
private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt ");
/**
@ -377,15 +377,21 @@ public final class Mp4Extractor implements Extractor, SeekMap {
long durationUs = C.TIME_UNSET;
List<Mp4Track> tracks = new ArrayList<>();
Metadata metadata = null;
// Process metadata.
Metadata udtaMetadata = null;
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
if (udta != null) {
metadata = AtomParsers.parseUdta(udta, isQuickTime);
if (metadata != null) {
gaplessInfoHolder.setFromMetadata(metadata);
udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime);
if (udtaMetadata != null) {
gaplessInfoHolder.setFromMetadata(udtaMetadata);
}
}
Metadata mdtaMetadata = null;
Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta);
if (meta != null) {
mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta);
}
boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
ArrayList<TrackSampleTable> trackSampleTables =
@ -401,15 +407,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
// Allow ten source samples per output sample, like the platform extractor.
int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
Format format = track.format.copyWithMaxInputSize(maxInputSize);
if (track.type == C.TRACK_TYPE_AUDIO) {
if (gaplessInfoHolder.hasGaplessInfo()) {
format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding);
}
if (metadata != null) {
format = format.copyWithMetadata(metadata);
}
}
format =
MetadataUtil.getFormatWithMetadata(
track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder);
mp4Track.trackOutput.format(format);
durationUs =
@ -716,24 +716,37 @@ public final class Mp4Extractor implements Extractor, SeekMap {
return false;
}
/**
* Returns whether the extractor should decode a leaf atom with type {@code atom}.
*/
/** Returns whether the extractor should decode a leaf atom with type {@code atom}. */
private static boolean shouldParseLeafAtom(int atom) {
return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr
|| atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss
|| atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc
|| atom == Atom.TYPE_stsz || atom == Atom.TYPE_stz2 || atom == Atom.TYPE_stco
|| atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp
|| atom == Atom.TYPE_udta;
return atom == Atom.TYPE_mdhd
|| atom == Atom.TYPE_mvhd
|| atom == Atom.TYPE_hdlr
|| atom == Atom.TYPE_stsd
|| atom == Atom.TYPE_stts
|| atom == Atom.TYPE_stss
|| atom == Atom.TYPE_ctts
|| atom == Atom.TYPE_elst
|| atom == Atom.TYPE_stsc
|| atom == Atom.TYPE_stsz
|| atom == Atom.TYPE_stz2
|| atom == Atom.TYPE_stco
|| atom == Atom.TYPE_co64
|| atom == Atom.TYPE_tkhd
|| atom == Atom.TYPE_ftyp
|| atom == Atom.TYPE_udta
|| atom == Atom.TYPE_keys
|| atom == Atom.TYPE_ilst;
}
/**
* Returns whether the extractor should decode a container atom with type {@code atom}.
*/
/** Returns whether the extractor should decode a container atom with type {@code atom}. */
private static boolean shouldParseContainerAtom(int atom) {
return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_edts;
return atom == Atom.TYPE_moov
|| atom == Atom.TYPE_trak
|| atom == Atom.TYPE_mdia
|| atom == Atom.TYPE_minf
|| atom == Atom.TYPE_stbl
|| atom == Atom.TYPE_edts
|| atom == Atom.TYPE_meta;
}
private static final class Mp4Track {

View File

@ -27,9 +27,7 @@ import java.io.IOException;
*/
/* package */ final class Sniffer {
/**
* The maximum number of bytes to peek when sniffing.
*/
/** The maximum number of bytes to peek when sniffing. */
private static final int SEARCH_LENGTH = 4 * 1024;
private static final int[] COMPATIBLE_BRANDS = new int[] {
@ -109,15 +107,19 @@ import java.io.IOException;
headerSize = Atom.LONG_HEADER_SIZE;
input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
buffer.setLimit(Atom.LONG_HEADER_SIZE);
atomSize = buffer.readUnsignedLongToLong();
atomSize = buffer.readLong();
} else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {
// The atom extends to the end of the file.
long endPosition = input.getLength();
if (endPosition != C.LENGTH_UNSET) {
atomSize = endPosition - input.getPosition() + headerSize;
long fileEndPosition = input.getLength();
if (fileEndPosition != C.LENGTH_UNSET) {
atomSize = fileEndPosition - input.getPeekPosition() + headerSize;
}
}
if (inputLength != C.LENGTH_UNSET && bytesSearched + atomSize > inputLength) {
// The file is invalid because the atom extends past the end of the file.
return false;
}
if (atomSize < headerSize) {
// The file is invalid because the atom size is too small for its header.
return false;
@ -125,6 +127,13 @@ import java.io.IOException;
bytesSearched += headerSize;
if (atomType == Atom.TYPE_moov) {
// We have seen the moov atom. We increase the search size to make sure we don't miss an
// mvex atom because the moov's size exceeds the search length.
bytesToSearch += (int) atomSize;
if (inputLength != C.LENGTH_UNSET && bytesToSearch > inputLength) {
// Make sure we don't exceed the file size.
bytesToSearch = (int) inputLength;
}
// Check for an mvex atom inside the moov atom to identify whether the file is fragmented.
continue;
}

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

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.audio.Ac3Util;
import com.google.android.exoplayer2.extractor.Extractor;
@ -140,7 +142,7 @@ public final class Ac3Extractor implements Extractor {
if (!startedPacket) {
// Pass data to the reader as though it's contained within a single infinitely long packet.
reader.packetStarted(firstSampleTimestampUs, true);
reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR);
startedPacket = true;
}
// TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes

View File

@ -100,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
@ -202,7 +204,7 @@ public final class AdtsExtractor implements Extractor {
if (!startedPacket) {
// Pass data to the reader as though it's contained within a single infinitely long packet.
reader.packetStarted(firstSampleTimestampUs, true);
reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR);
startedPacket = true;
}
// TODO: Make it possible for reader to consume the dataSource directly, so that it becomes

View File

@ -141,7 +141,7 @@ public final class AdtsReader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}

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

@ -80,7 +80,7 @@ public final class DtsReader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -73,8 +75,8 @@ public final class DvbSubtitleReader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (!dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
return;
}
writingSample = true;

View File

@ -43,9 +43,9 @@ public interface ElementaryStreamReader {
* Called when a packet starts.
*
* @param pesTimeUs The timestamp associated with the packet.
* @param dataAlignmentIndicator The data alignment indicator associated with the packet.
* @param flags See {@link TsPayloadReader.Flags}.
*/
void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator);
void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags);
/**
* Consumes (possibly partial) data from the current packet.

View File

@ -107,7 +107,8 @@ public final class H262Reader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
// TODO (Internal b/32267012): Consider using random access indicator.
this.pesTimeUs = pesTimeUs;
}

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@ -56,9 +58,12 @@ public final class H264Reader implements ElementaryStreamReader {
// State that should not be reset on seek.
private boolean hasOutputFormat;
// Per packet state that gets reset at the start of each packet.
// Per PES packet state that gets reset at the start of each PES packet.
private long pesTimeUs;
// State inherited from the TS packet header.
private boolean randomAccessIndicator;
// Scratch variables to avoid allocations.
private final ParsableByteArray seiWrapper;
@ -88,6 +93,7 @@ public final class H264Reader implements ElementaryStreamReader {
sei.reset();
sampleReader.reset();
totalBytesWritten = 0;
randomAccessIndicator = false;
}
@Override
@ -100,8 +106,9 @@ public final class H264Reader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
this.pesTimeUs = pesTimeUs;
randomAccessIndicator |= (flags & FLAG_RANDOM_ACCESS_INDICATOR) != 0;
}
@Override
@ -220,12 +227,17 @@ public final class H264Reader implements ElementaryStreamReader {
seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.
seiReader.consume(pesTimeUs, seiWrapper);
}
sampleReader.endNalUnit(position, offset);
boolean sampleIsKeyFrame =
sampleReader.endNalUnit(position, offset, hasOutputFormat, randomAccessIndicator);
if (sampleIsKeyFrame) {
// This is either an IDR frame or the first I-frame since the random access indicator, so mark
// it as a keyframe. Clear the flag so that subsequent non-IDR I-frames are not marked as
// keyframes until we see another random access indicator.
randomAccessIndicator = false;
}
}
/**
* Consumes a stream of NAL units and outputs samples.
*/
/** Consumes a stream of NAL units and outputs samples. */
private static final class SampleReader {
private static final int DEFAULT_BUFFER_SIZE = 128;
@ -430,11 +442,12 @@ public final class H264Reader implements ElementaryStreamReader {
isFilling = false;
}
public void endNalUnit(long position, int offset) {
public boolean endNalUnit(
long position, int offset, boolean hasOutputFormat, boolean randomAccessIndicator) {
if (nalUnitType == NAL_UNIT_TYPE_AUD
|| (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) {
// If the NAL unit ending is the start of a new sample, output the previous one.
if (readingSample) {
if (hasOutputFormat && readingSample) {
int nalUnitLength = (int) (position - nalUnitStartPosition);
outputSample(offset + nalUnitLength);
}
@ -443,8 +456,12 @@ public final class H264Reader implements ElementaryStreamReader {
sampleIsKeyframe = false;
readingSample = true;
}
sampleIsKeyframe |= nalUnitType == NAL_UNIT_TYPE_IDR || (allowNonIdrKeyframes
&& nalUnitType == NAL_UNIT_TYPE_NON_IDR && sliceHeader.isISlice());
boolean treatIFrameAsKeyframe =
allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator;
sampleIsKeyframe |=
nalUnitType == NAL_UNIT_TYPE_IDR
|| (treatIFrameAsKeyframe && nalUnitType == NAL_UNIT_TYPE_NON_IDR);
return sampleIsKeyframe;
}
private void outputSample(int offset) {
@ -486,10 +503,21 @@ public final class H264Reader implements ElementaryStreamReader {
hasSliceType = true;
}
public void setAll(SpsData spsData, int nalRefIdc, int sliceType, int frameNum,
int picParameterSetId, boolean fieldPicFlag, boolean bottomFieldFlagPresent,
boolean bottomFieldFlag, boolean idrPicFlag, int idrPicId, int picOrderCntLsb,
int deltaPicOrderCntBottom, int deltaPicOrderCnt0, int deltaPicOrderCnt1) {
public void setAll(
SpsData spsData,
int nalRefIdc,
int sliceType,
int frameNum,
int picParameterSetId,
boolean fieldPicFlag,
boolean bottomFieldFlagPresent,
boolean bottomFieldFlag,
boolean idrPicFlag,
int idrPicId,
int picOrderCntLsb,
int deltaPicOrderCntBottom,
int deltaPicOrderCnt0,
int deltaPicOrderCnt1) {
this.spsData = spsData;
this.nalRefIdc = nalRefIdc;
this.sliceType = sliceType;
@ -514,23 +542,26 @@ public final class H264Reader implements ElementaryStreamReader {
private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) {
// See ISO 14496-10 subsection 7.4.1.2.4.
return isComplete && (!other.isComplete || frameNum != other.frameNum
|| picParameterSetId != other.picParameterSetId || fieldPicFlag != other.fieldPicFlag
|| (bottomFieldFlagPresent && other.bottomFieldFlagPresent
&& bottomFieldFlag != other.bottomFieldFlag)
|| (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
|| (spsData.picOrderCountType == 0 && other.spsData.picOrderCountType == 0
&& (picOrderCntLsb != other.picOrderCntLsb
|| deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))
|| (spsData.picOrderCountType == 1 && other.spsData.picOrderCountType == 1
&& (deltaPicOrderCnt0 != other.deltaPicOrderCnt0
|| deltaPicOrderCnt1 != other.deltaPicOrderCnt1))
|| idrPicFlag != other.idrPicFlag
|| (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId));
return isComplete
&& (!other.isComplete
|| frameNum != other.frameNum
|| picParameterSetId != other.picParameterSetId
|| fieldPicFlag != other.fieldPicFlag
|| (bottomFieldFlagPresent
&& other.bottomFieldFlagPresent
&& bottomFieldFlag != other.bottomFieldFlag)
|| (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
|| (spsData.picOrderCountType == 0
&& other.spsData.picOrderCountType == 0
&& (picOrderCntLsb != other.picOrderCntLsb
|| deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))
|| (spsData.picOrderCountType == 1
&& other.spsData.picOrderCountType == 1
&& (deltaPicOrderCnt0 != other.deltaPicOrderCnt0
|| deltaPicOrderCnt1 != other.deltaPicOrderCnt1))
|| idrPicFlag != other.idrPicFlag
|| (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId));
}
}
}
}

View File

@ -104,7 +104,8 @@ public final class H265Reader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
// TODO (Internal b/32267012): Consider using random access indicator.
this.pesTimeUs = pesTimeUs;
}

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -63,8 +65,8 @@ public final class Id3Reader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (!dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
return;
}
writingSample = true;

View File

@ -93,7 +93,7 @@ public final class LatmReader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}

View File

@ -83,7 +83,7 @@ public final class MpegAudioReader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}

View File

@ -78,9 +78,8 @@ public final class PesReader implements TsPayloadReader {
}
@Override
public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator)
throws ParserException {
if (payloadUnitStartIndicator) {
public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException {
if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) {
switch (state) {
case STATE_FINDING_HEADER:
case STATE_READING_HEADER:
@ -122,7 +121,8 @@ public final class PesReader implements TsPayloadReader {
if (continueRead(data, pesScratch.data, readLength)
&& continueRead(data, null, extendedHeaderLength)) {
parseHeaderExtension();
reader.packetStarted(timeUs, dataAlignmentIndicator);
flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;
reader.packetStarted(timeUs, flags);
setState(STATE_READING_BODY);
}
break;

View File

@ -343,7 +343,7 @@ public final class PsExtractor implements Extractor {
data.readBytes(pesScratch.data, 0, extendedHeaderLength);
pesScratch.setPosition(0);
parseHeaderExtension();
pesPayloadReader.packetStarted(timeUs, true);
pesPayloadReader.packetStarted(timeUs, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR);
pesPayloadReader.consume(data);
// We always have complete PES packets with program stream.
pesPayloadReader.packetFinished();

View File

@ -57,7 +57,8 @@ public final class SectionReader implements TsPayloadReader {
}
@Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
public void consume(ParsableByteArray data, @Flags int flags) {
boolean payloadUnitStartIndicator = (flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0;
int payloadStartPosition = C.POSITION_UNSET;
if (payloadUnitStartIndicator) {
int payloadStartOffset = data.readUnsignedByte();

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR;
import android.support.annotation.IntDef;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@ -279,6 +281,8 @@ public final class TsExtractor implements Extractor {
return RESULT_CONTINUE;
}
@TsPayloadReader.Flags int packetHeaderFlags = 0;
// Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
int tsPacketHeader = tsPacketBuffer.readInt();
if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator
@ -286,7 +290,7 @@ public final class TsExtractor implements Extractor {
tsPacketBuffer.setPosition(endOfPacket);
return RESULT_CONTINUE;
}
boolean payloadUnitStartIndicator = (tsPacketHeader & 0x400000) != 0;
packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0;
// Ignoring transport_priority (tsPacketHeader & 0x200000)
int pid = (tsPacketHeader & 0x1FFF00) >> 8;
// Ignoring transport_scrambling_control (tsPacketHeader & 0xC0)
@ -317,14 +321,20 @@ public final class TsExtractor implements Extractor {
// Skip the adaptation field.
if (adaptationFieldExists) {
int adaptationFieldLength = tsPacketBuffer.readUnsignedByte();
tsPacketBuffer.skipBytes(adaptationFieldLength);
int adaptationFieldFlags = tsPacketBuffer.readUnsignedByte();
packetHeaderFlags |=
(adaptationFieldFlags & 0x40) != 0 // random_access_indicator.
? TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR
: 0;
tsPacketBuffer.skipBytes(adaptationFieldLength - 1 /* flags */);
}
// Read the payload.
boolean wereTracksEnded = tracksEnded;
if (shouldConsumePacketPayload(pid)) {
tsPacketBuffer.setLimit(endOfPacket);
payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator);
payloadReader.consume(tsPacketBuffer, packetHeaderFlags);
tsPacketBuffer.setLimit(limit);
}
if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) {

View File

@ -15,12 +15,16 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import android.support.annotation.IntDef;
import android.util.SparseArray;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.List;
@ -174,6 +178,29 @@ public interface TsPayloadReader {
}
/**
* Contextual flags indicating the presence of indicators in the TS packet or PES packet headers.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {
FLAG_PAYLOAD_UNIT_START_INDICATOR,
FLAG_RANDOM_ACCESS_INDICATOR,
FLAG_DATA_ALIGNMENT_INDICATOR
})
@interface Flags {}
/** Indicates the presence of the payload_unit_start_indicator in the TS packet header. */
int FLAG_PAYLOAD_UNIT_START_INDICATOR = 1;
/**
* Indicates the presence of the random_access_indicator in the TS packet header adaptation field.
*/
int FLAG_RANDOM_ACCESS_INDICATOR = 1 << 1;
/** Indicates the presence of the data_alignment_indicator in the PES header. */
int FLAG_DATA_ALIGNMENT_INDICATOR = 1 << 2;
/**
* Initializes the payload reader.
*
@ -187,10 +214,10 @@ public interface TsPayloadReader {
/**
* Notifies the reader that a seek has occurred.
* <p>
* Following a call to this method, the data passed to the next invocation of
* {@link #consume(ParsableByteArray, boolean)} will not be a continuation of the data that was
* previously passed. Hence the reader should reset any internal state.
*
* <p>Following a call to this method, the data passed to the next invocation of {@link #consume}
* will not be a continuation of the data that was previously passed. Hence the reader should
* reset any internal state.
*/
void seek();
@ -198,9 +225,8 @@ public interface TsPayloadReader {
* Consumes the payload of a TS packet.
*
* @param data The TS packet. The position will be set to the start of the payload.
* @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet.
* @param flags See {@link Flags}.
* @throws ParserException If the payload could not be parsed.
*/
void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) throws ParserException;
void consume(ParsableByteArray data, @Flags int flags) throws ParserException;
}

View File

@ -248,9 +248,15 @@ public final class MediaCodecInfo {
// If we don't know any better, we assume that the profile and level are supported.
return true;
}
int profile = codecProfileAndLevel.first;
int level = codecProfileAndLevel.second;
if (!isVideo && profile != CodecProfileLevel.AACObjectXHE) {
// Some devices/builds underreport audio capabilities, so assume support except for xHE-AAC
// which may not be widely supported. See https://github.com/google/ExoPlayer/issues/5145.
return true;
}
for (CodecProfileLevel capabilities : getProfileLevels()) {
if (capabilities.profile == codecProfileAndLevel.first
&& capabilities.level >= codecProfileAndLevel.second) {
if (capabilities.profile == profile && capabilities.level >= level) {
return true;
}
}

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;
@ -239,14 +240,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 +295,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;
@ -457,29 +467,36 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return;
}
drmSession = pendingDrmSession;
setCodecDrmSession(sourceDrmSession);
String mimeType = inputFormat.sampleMimeType;
MediaCrypto wrappedMediaCrypto = null;
boolean drmSessionRequiresSecureDecoder = false;
if (drmSession != null) {
FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto();
if (codecDrmSession != null) {
if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError();
if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
// input format causes the session to be replaced before it's used.
FrameworkMediaCrypto sessionMediaCrypto = codecDrmSession.getMediaCrypto();
if (sessionMediaCrypto == null) {
DrmSessionException drmError = codecDrmSession.getError();
if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a
// new input format causes the session to be replaced before it's used.
} else {
// The drm session isn't open yet.
return;
}
} else {
// The drm session isn't open yet.
return;
try {
mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId);
} catch (MediaCryptoException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
mediaCryptoRequiresSecureDecoder =
!sessionMediaCrypto.forceAllowInsecureDecoderComponents
&& mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
} else {
wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
@DrmSession.State int drmSessionState = drmSession.getState();
@DrmSession.State int drmSessionState = codecDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
} else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
// Wait for keys.
return;
@ -488,7 +505,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
try {
maybeInitCodecWithFallback(wrappedMediaCrypto, drmSessionRequiresSecureDecoder);
maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder);
} catch (DecoderInitializationException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
@ -537,7 +554,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 +569,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Override
protected void onDisabled() {
inputFormat = null;
if (drmSession != null || pendingDrmSession != null) {
if (sourceDrmSession != null || codecDrmSession != null) {
// TODO: Do something better with this case.
onReset();
} else {
@ -565,51 +582,40 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
try {
releaseCodec();
} finally {
try {
if (drmSession != null) {
drmSessionManager.releaseSession(drmSession);
}
} finally {
try {
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} finally {
drmSession = null;
pendingDrmSession = null;
}
}
setSourceDrmSession(null);
}
}
protected void releaseCodec() {
availableCodecInfos = null;
if (codec != null) {
codecInfo = null;
codecFormat = null;
resetInputBuffer();
resetOutputBuffer();
resetCodecBuffers();
waitingForKeys = false;
codecHotswapDeadlineMs = C.TIME_UNSET;
decodeOnlyPresentationTimestamps.clear();
decoderCounters.decoderReleaseCount++;
try {
codec.stop();
} finally {
codecInfo = null;
codecFormat = null;
resetInputBuffer();
resetOutputBuffer();
resetCodecBuffers();
waitingForKeys = false;
codecHotswapDeadlineMs = C.TIME_UNSET;
decodeOnlyPresentationTimestamps.clear();
try {
if (codec != null) {
decoderCounters.decoderReleaseCount++;
try {
codec.release();
codec.stop();
} finally {
codec = null;
if (drmSession != null && pendingDrmSession != drmSession) {
try {
drmSessionManager.releaseSession(drmSession);
} finally {
drmSession = null;
}
}
codec.release();
}
}
} finally {
codec = null;
try {
if (mediaCrypto != null) {
mediaCrypto.release();
}
} finally {
mediaCrypto = null;
mediaCryptoRequiresSecureDecoder = false;
setCodecDrmSession(null);
}
}
}
@ -680,12 +686,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 +738,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 +758,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
throw new DecoderInitializationException(
inputFormat,
/* cause= */ null,
drmSessionRequiresSecureDecoder,
mediaCryptoRequiresSecureDecoder,
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);
}
@ -768,7 +777,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 +793,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 +937,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 +1109,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 +1153,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
}
pendingDrmSession =
DrmSession<FrameworkMediaCrypto> session =
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
if (pendingDrmSession == drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
if (session == sourceDrmSession || session == codecDrmSession) {
// We already had this session. The manager must be reference counting, so release it once
// to get the count attributed to this renderer back down to 1.
drmSessionManager.releaseSession(session);
}
setSourceDrmSession(session);
} else {
pendingDrmSession = null;
setSourceDrmSession(null);
}
}
@ -1143,40 +1173,58 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
// We have an existing codec that we may need to reconfigure or re-initialize. If the existing
// codec instance is being kept then its operating rate may need to be updated.
if (pendingDrmSession != drmSession) {
if ((sourceDrmSession == null && codecDrmSession != null)
|| (sourceDrmSession != null && codecDrmSession == null)
|| (sourceDrmSession != null && !codecInfo.secure)
|| (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) {
// We might need to switch between the clear and protected output paths, or we're using DRM
// prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM
// session.
drainAndReinitializeCodec();
} else {
switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {
case KEEP_CODEC_RESULT_NO:
drainAndReinitializeCodec();
break;
case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
return;
}
switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {
case KEEP_CODEC_RESULT_NO:
drainAndReinitializeCodec();
break;
case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
codecFormat = newFormat;
updateCodecOperatingRate();
if (sourceDrmSession != codecDrmSession) {
drainAndUpdateCodecDrmSession();
} else {
drainAndFlushCodec();
}
break;
case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION:
if (codecNeedsReconfigureWorkaround) {
drainAndReinitializeCodec();
} else {
codecReconfigured = true;
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
codecNeedsAdaptationWorkaroundBuffer =
codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS
|| (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION
&& newFormat.width == codecFormat.width
&& newFormat.height == codecFormat.height);
codecFormat = newFormat;
updateCodecOperatingRate();
break;
case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION:
if (codecNeedsReconfigureWorkaround) {
drainAndReinitializeCodec();
} else {
codecReconfigured = true;
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
codecNeedsAdaptationWorkaroundBuffer =
codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS
|| (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION
&& newFormat.width == codecFormat.width
&& newFormat.height == codecFormat.height);
codecFormat = newFormat;
updateCodecOperatingRate();
if (sourceDrmSession != codecDrmSession) {
drainAndUpdateCodecDrmSession();
}
break;
case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:
codecFormat = newFormat;
updateCodecOperatingRate();
break;
default:
throw new IllegalStateException(); // Never happens.
}
}
break;
case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:
codecFormat = newFormat;
updateCodecOperatingRate();
if (sourceDrmSession != codecDrmSession) {
drainAndUpdateCodecDrmSession();
}
break;
default:
throw new IllegalStateException(); // Never happens.
}
}
@ -1311,6 +1359,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 +1392,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 +1596,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 +1612,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.
@ -1693,7 +1798,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/
private static boolean codecNeedsEosFlushWorkaround(String name) {
return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name))
|| (Util.SDK_INT <= 19 && "hb2000".equals(Util.DEVICE)
|| (Util.SDK_INT <= 19
&& ("hb2000".equals(Util.DEVICE) || "stvm8".equals(Util.DEVICE))
&& ("OMX.amlogic.avc.decoder.awesome".equals(name)
|| "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
}

View File

@ -318,7 +318,23 @@ public final class MediaCodecUtil {
}
// Work around https://github.com/google/ExoPlayer/issues/4519.
if ("OMX.SEC.mp3.dec".equals(name) && "SM-T530".equals(Util.MODEL)) {
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")
|| Util.MODEL.startsWith("SCH-I535")
|| Util.MODEL.startsWith("SPH-L710"))) {
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;
}

View File

@ -18,8 +18,10 @@ package com.google.android.exoplayer2.metadata;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* A collection of metadata entries.
@ -76,6 +78,18 @@ public final class Metadata implements Parcelable {
return entries[index];
}
/**
* Returns a copy of this metadata with the specified entries appended.
*
* @param entriesToAppend The entries to append.
* @return The metadata instance with the appended entries.
*/
public Metadata copyWithAppendedEntries(Entry... entriesToAppend) {
@NullableType Entry[] merged = Arrays.copyOf(entries, entries.length + entriesToAppend.length);
System.arraycopy(entriesToAppend, 0, merged, entries.length, entriesToAppend.length);
return new Metadata(Util.castNonNullTypeArray(merged));
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
import com.google.android.exoplayer2.metadata.icy.IcyDecoder;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder;
import com.google.android.exoplayer2.util.MimeTypes;
@ -46,38 +47,43 @@ public interface MetadataDecoderFactory {
/**
* Default {@link MetadataDecoder} implementation.
* <p>
* The formats supported by this factory are:
*
* <p>The formats supported by this factory are:
*
* <ul>
* <li>ID3 ({@link Id3Decoder})</li>
* <li>EMSG ({@link EventMessageDecoder})</li>
* <li>SCTE-35 ({@link SpliceInfoDecoder})</li>
* <li>ID3 ({@link Id3Decoder})
* <li>EMSG ({@link EventMessageDecoder})
* <li>SCTE-35 ({@link SpliceInfoDecoder})
* <li>ICY ({@link IcyDecoder})
* </ul>
*/
MetadataDecoderFactory DEFAULT = new MetadataDecoderFactory() {
MetadataDecoderFactory DEFAULT =
new MetadataDecoderFactory() {
@Override
public boolean supportsFormat(Format format) {
String mimeType = format.sampleMimeType;
return MimeTypes.APPLICATION_ID3.equals(mimeType)
|| MimeTypes.APPLICATION_EMSG.equals(mimeType)
|| MimeTypes.APPLICATION_SCTE35.equals(mimeType);
}
@Override
public MetadataDecoder createDecoder(Format format) {
switch (format.sampleMimeType) {
case MimeTypes.APPLICATION_ID3:
return new Id3Decoder();
case MimeTypes.APPLICATION_EMSG:
return new EventMessageDecoder();
case MimeTypes.APPLICATION_SCTE35:
return new SpliceInfoDecoder();
default:
throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
}
}
};
@Override
public boolean supportsFormat(Format format) {
String mimeType = format.sampleMimeType;
return MimeTypes.APPLICATION_ID3.equals(mimeType)
|| MimeTypes.APPLICATION_EMSG.equals(mimeType)
|| MimeTypes.APPLICATION_SCTE35.equals(mimeType)
|| MimeTypes.APPLICATION_ICY.equals(mimeType);
}
@Override
public MetadataDecoder createDecoder(Format format) {
switch (format.sampleMimeType) {
case MimeTypes.APPLICATION_ID3:
return new Id3Decoder();
case MimeTypes.APPLICATION_EMSG:
return new EventMessageDecoder();
case MimeTypes.APPLICATION_SCTE35:
return new SpliceInfoDecoder();
case MimeTypes.APPLICATION_ICY:
return new IcyDecoder();
default:
throw new IllegalArgumentException(
"Attempted to create decoder for unsupported format");
}
}
};
}

View File

@ -0,0 +1,73 @@
/*
* 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.metadata.icy;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Decodes ICY stream information. */
public final class IcyDecoder implements MetadataDecoder {
private static final String TAG = "IcyDecoder";
private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';");
private static final String STREAM_KEY_NAME = "streamtitle";
private static final String STREAM_KEY_URL = "streamurl";
@Override
@Nullable
@SuppressWarnings("ByteBufferBackingArray")
public Metadata decode(MetadataInputBuffer inputBuffer) {
ByteBuffer buffer = inputBuffer.data;
byte[] data = buffer.array();
int length = buffer.limit();
return decode(Util.fromUtf8Bytes(data, 0, length));
}
@Nullable
@VisibleForTesting
/* package */ Metadata decode(String metadata) {
String name = null;
String url = null;
int index = 0;
Matcher matcher = METADATA_ELEMENT.matcher(metadata);
while (matcher.find(index)) {
String key = Util.toLowerInvariant(matcher.group(1));
String value = matcher.group(2);
switch (key) {
case STREAM_KEY_NAME:
name = value;
break;
case STREAM_KEY_URL:
url = value;
break;
default:
Log.w(TAG, "Unrecognized ICY tag: " + name);
break;
}
index = matcher.end();
}
return (name != null || url != null) ? new Metadata(new IcyInfo(name, url)) : null;
}
}

View File

@ -0,0 +1,243 @@
/*
* 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.metadata.icy;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
import java.util.Map;
/** ICY headers. */
public final class IcyHeaders implements Metadata.Entry {
public static final String REQUEST_HEADER_ENABLE_METADATA_NAME = "Icy-MetaData";
public static final String REQUEST_HEADER_ENABLE_METADATA_VALUE = "1";
private static final String TAG = "IcyHeaders";
private static final String RESPONSE_HEADER_BITRATE = "icy-br";
private static final String RESPONSE_HEADER_GENRE = "icy-genre";
private static final String RESPONSE_HEADER_NAME = "icy-name";
private static final String RESPONSE_HEADER_URL = "icy-url";
private static final String RESPONSE_HEADER_PUB = "icy-pub";
private static final String RESPONSE_HEADER_METADATA_INTERVAL = "icy-metaint";
/**
* Parses {@link IcyHeaders} from response headers.
*
* @param responseHeaders The response headers.
* @return The parsed {@link IcyHeaders}, or {@code null} if no ICY headers were present.
*/
@Nullable
public static IcyHeaders parse(Map<String, List<String>> responseHeaders) {
boolean icyHeadersPresent = false;
int bitrate = Format.NO_VALUE;
String genre = null;
String name = null;
String url = null;
boolean isPublic = false;
int metadataInterval = C.LENGTH_UNSET;
List<String> headers = responseHeaders.get(RESPONSE_HEADER_BITRATE);
if (headers != null) {
String bitrateHeader = headers.get(0);
try {
bitrate = Integer.parseInt(bitrateHeader) * 1000;
if (bitrate > 0) {
icyHeadersPresent = true;
} else {
Log.w(TAG, "Invalid bitrate: " + bitrateHeader);
bitrate = Format.NO_VALUE;
}
} catch (NumberFormatException e) {
Log.w(TAG, "Invalid bitrate header: " + bitrateHeader);
}
}
headers = responseHeaders.get(RESPONSE_HEADER_GENRE);
if (headers != null) {
genre = headers.get(0);
icyHeadersPresent = true;
}
headers = responseHeaders.get(RESPONSE_HEADER_NAME);
if (headers != null) {
name = headers.get(0);
icyHeadersPresent = true;
}
headers = responseHeaders.get(RESPONSE_HEADER_URL);
if (headers != null) {
url = headers.get(0);
icyHeadersPresent = true;
}
headers = responseHeaders.get(RESPONSE_HEADER_PUB);
if (headers != null) {
isPublic = headers.get(0).equals("1");
icyHeadersPresent = true;
}
headers = responseHeaders.get(RESPONSE_HEADER_METADATA_INTERVAL);
if (headers != null) {
String metadataIntervalHeader = headers.get(0);
try {
metadataInterval = Integer.parseInt(metadataIntervalHeader);
if (metadataInterval > 0) {
icyHeadersPresent = true;
} else {
Log.w(TAG, "Invalid metadata interval: " + metadataIntervalHeader);
metadataInterval = C.LENGTH_UNSET;
}
} catch (NumberFormatException e) {
Log.w(TAG, "Invalid metadata interval: " + metadataIntervalHeader);
}
}
return icyHeadersPresent
? new IcyHeaders(bitrate, genre, name, url, isPublic, metadataInterval)
: null;
}
/**
* Bitrate in bits per second ({@code (icy-br * 1000)}), or {@link Format#NO_VALUE} if the header
* was not present.
*/
public final int bitrate;
/** The genre ({@code icy-genre}). */
@Nullable public final String genre;
/** The stream name ({@code icy-name}). */
@Nullable public final String name;
/** The URL of the radio station ({@code icy-url}). */
@Nullable public final String url;
/**
* Whether the radio station is listed ({@code icy-pub}), or {@code false} if the header was not
* present.
*/
public final boolean isPublic;
/**
* The interval in bytes between metadata chunks ({@code icy-metaint}), or {@link C#LENGTH_UNSET}
* if the header was not present.
*/
public final int metadataInterval;
/**
* @param bitrate See {@link #bitrate}.
* @param genre See {@link #genre}.
* @param name See {@link #name See}.
* @param url See {@link #url}.
* @param isPublic See {@link #isPublic}.
* @param metadataInterval See {@link #metadataInterval}.
*/
public IcyHeaders(
int bitrate,
@Nullable String genre,
@Nullable String name,
@Nullable String url,
boolean isPublic,
int metadataInterval) {
Assertions.checkArgument(metadataInterval == C.LENGTH_UNSET || metadataInterval > 0);
this.bitrate = bitrate;
this.genre = genre;
this.name = name;
this.url = url;
this.isPublic = isPublic;
this.metadataInterval = metadataInterval;
}
/* package */ IcyHeaders(Parcel in) {
bitrate = in.readInt();
genre = in.readString();
name = in.readString();
url = in.readString();
isPublic = Util.readBoolean(in);
metadataInterval = in.readInt();
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
IcyHeaders other = (IcyHeaders) obj;
return bitrate == other.bitrate
&& Util.areEqual(genre, other.genre)
&& Util.areEqual(name, other.name)
&& Util.areEqual(url, other.url)
&& isPublic == other.isPublic
&& metadataInterval == other.metadataInterval;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + bitrate;
result = 31 * result + (genre != null ? genre.hashCode() : 0);
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (url != null ? url.hashCode() : 0);
result = 31 * result + (isPublic ? 1 : 0);
result = 31 * result + metadataInterval;
return result;
}
@Override
public String toString() {
return "IcyHeaders: name=\""
+ name
+ "\", genre=\""
+ genre
+ "\", bitrate="
+ bitrate
+ ", metadataInterval="
+ metadataInterval;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bitrate);
dest.writeString(genre);
dest.writeString(name);
dest.writeString(url);
Util.writeBoolean(dest, isPublic);
dest.writeInt(metadataInterval);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<IcyHeaders> CREATOR =
new Parcelable.Creator<IcyHeaders>() {
@Override
public IcyHeaders createFromParcel(Parcel in) {
return new IcyHeaders(in);
}
@Override
public IcyHeaders[] newArray(int size) {
return new IcyHeaders[size];
}
};
}

View File

@ -0,0 +1,97 @@
/*
* 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.metadata.icy;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Util;
/** ICY in-stream information. */
public final class IcyInfo implements Metadata.Entry {
/** The stream title if present, or {@code null}. */
@Nullable public final String title;
/** The stream title if present, or {@code null}. */
@Nullable public final String url;
/**
* @param title See {@link #title}.
* @param url See {@link #url}.
*/
public IcyInfo(@Nullable String title, @Nullable String url) {
this.title = title;
this.url = url;
}
/* package */ IcyInfo(Parcel in) {
title = in.readString();
url = in.readString();
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
IcyInfo other = (IcyInfo) obj;
return Util.areEqual(title, other.title) && Util.areEqual(url, other.url);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (title != null ? title.hashCode() : 0);
result = 31 * result + (url != null ? url.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "ICY: title=\"" + title + "\", url=\"" + url + "\"";
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(title);
dest.writeString(url);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<IcyInfo> CREATOR =
new Parcelable.Creator<IcyInfo>() {
@Override
public IcyInfo createFromParcel(Parcel in) {
return new IcyInfo(in);
}
@Override
public IcyInfo[] newArray(int size) {
return new IcyInfo[size];
}
};
}

View File

@ -0,0 +1,357 @@
/*
* 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 final DatabaseProvider databaseProvider;
@Nullable private DownloadsTable downloadTable;
/**
* 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) {
return getDownloadTable().get(id);
}
@Override
public DownloadStateCursor getDownloadStates(@DownloadState.State int... states) {
return getDownloadTable().get(states);
}
@Override
public void putDownloadState(DownloadState downloadState) {
getDownloadTable().replace(downloadState);
}
@Override
public void removeDownloadState(String id) {
getDownloadTable().delete(id);
}
private DownloadsTable getDownloadTable() {
if (downloadTable == null) {
downloadTable = new DownloadsTable(databaseProvider);
}
return downloadTable;
}
private static final class DownloadStateCursorImpl implements DownloadStateCursor {
private final Cursor cursor;
private DownloadStateCursorImpl(Cursor cursor) {
this.cursor = cursor;
}
@Override
public DownloadState getDownloadState() {
return DownloadsTable.getDownloadState(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();
}
}
private static final class DownloadsTable {
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_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_START_TIME_MS = 10;
private static final int COLUMN_INDEX_UPDATE_TIME_MS = 11;
private static final int COLUMN_INDEX_STREAM_KEYS = 12;
private static final int COLUMN_INDEX_CUSTOM_METADATA = 13;
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_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_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;
public DownloadsTable(DatabaseProvider databaseProvider) {
this.databaseProvider = databaseProvider;
VersionTable versionTable = new VersionTable(databaseProvider);
int version = versionTable.getVersion(VersionTable.FEATURE_OFFLINE);
if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) {
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.beginTransaction();
try {
writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS);
writableDatabase.execSQL(SQL_CREATE_TABLE);
versionTable.setVersion(VersionTable.FEATURE_OFFLINE, TABLE_VERSION);
writableDatabase.setTransactionSuccessful();
} finally {
writableDatabase.endTransaction();
}
} else if (version < TABLE_VERSION) {
// There is no previous version currently.
throw new IllegalStateException();
}
}
public void replace(DownloadState downloadState) {
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_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);
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
}
@Nullable
public DownloadState get(String id) {
String[] selectionArgs = {id};
try (Cursor cursor = query(COLUMN_SELECTION_ID, selectionArgs)) {
if (cursor.getCount() == 0) {
return null;
}
cursor.moveToNext();
DownloadState downloadState = getDownloadState(cursor);
Assertions.checkState(id.equals(downloadState.id));
return downloadState;
}
}
public DownloadStateCursor get(@DownloadState.State int... states) {
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 = query(selection, /* selectionArgs= */ null);
return new DownloadStateCursorImpl(cursor);
}
public void delete(String id) {
String[] selectionArgs = {id};
databaseProvider.getWritableDatabase().delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs);
}
private Cursor query(@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 getDownloadState(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.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;
}
}
}

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