mirror of
https://github.com/androidx/media.git
synced 2025-05-12 01:59:50 +08:00
Merge pull request #3 from google/dev-v2
Merge pull request #3 from google/dev-v2
This commit is contained in:
commit
20aeb0c8aa
9
.gitignore
vendored
9
.gitignore
vendored
@ -37,6 +37,12 @@ local.properties
|
|||||||
proguard.cfg
|
proguard.cfg
|
||||||
proguard-project.txt
|
proguard-project.txt
|
||||||
|
|
||||||
|
# Bazel
|
||||||
|
bazel-bin
|
||||||
|
bazel-genfiles
|
||||||
|
bazel-out
|
||||||
|
bazel-testlogs
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
.DS_Store
|
.DS_Store
|
||||||
cmake-build-debug
|
cmake-build-debug
|
||||||
@ -66,3 +72,6 @@ extensions/cronet/jniLibs/*
|
|||||||
extensions/cronet/libs/*
|
extensions/cronet/libs/*
|
||||||
!extensions/cronet/libs/README.md
|
!extensions/cronet/libs/README.md
|
||||||
|
|
||||||
|
# Cast receiver
|
||||||
|
cast_receiver_app/external-js
|
||||||
|
cast_receiver_app/bazel-cast_receiver_app
|
||||||
|
10
.hgignore
10
.hgignore
@ -44,6 +44,12 @@ local.properties
|
|||||||
proguard.cfg
|
proguard.cfg
|
||||||
proguard-project.txt
|
proguard-project.txt
|
||||||
|
|
||||||
|
# Bazel
|
||||||
|
bazel-bin
|
||||||
|
bazel-genfiles
|
||||||
|
bazel-out
|
||||||
|
bazel-testlogs
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
.DS_Store
|
.DS_Store
|
||||||
cmake-build-debug
|
cmake-build-debug
|
||||||
@ -69,3 +75,7 @@ extensions/cronet/jniLibs/*
|
|||||||
!extensions/cronet/jniLibs/README.md
|
!extensions/cronet/jniLibs/README.md
|
||||||
extensions/cronet/libs/*
|
extensions/cronet/libs/*
|
||||||
!extensions/cronet/libs/README.md
|
!extensions/cronet/libs/README.md
|
||||||
|
|
||||||
|
# Cast receiver
|
||||||
|
cast_receiver_app/external-js
|
||||||
|
cast_receiver_app/bazel-cast_receiver_app
|
||||||
|
40
README.md
40
README.md
@ -27,6 +27,8 @@ repository and depend on the modules locally.
|
|||||||
|
|
||||||
### From JCenter ###
|
### From JCenter ###
|
||||||
|
|
||||||
|
#### 1. Add repositories ####
|
||||||
|
|
||||||
The easiest way to get started using ExoPlayer is to add it as a gradle
|
The easiest way to get started using ExoPlayer is to add it as a gradle
|
||||||
dependency. You need to make sure you have the Google and JCenter repositories
|
dependency. You need to make sure you have the Google and JCenter repositories
|
||||||
included in the `build.gradle` file in the root of your project:
|
included in the `build.gradle` file in the root of your project:
|
||||||
@ -38,6 +40,8 @@ repositories {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 2. Add ExoPlayer module dependencies ####
|
||||||
|
|
||||||
Next add a dependency in the `build.gradle` file of your app module. The
|
Next add a dependency in the `build.gradle` file of your app module. The
|
||||||
following will add a dependency to the full library:
|
following will add a dependency to the full library:
|
||||||
|
|
||||||
@ -45,15 +49,7 @@ following will add a dependency to the full library:
|
|||||||
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
||||||
```
|
```
|
||||||
|
|
||||||
where `2.X.X` is your preferred version. If not enabled already, you also need
|
where `2.X.X` is your preferred version.
|
||||||
to turn on Java 8 support in all `build.gradle` files depending on ExoPlayer, by
|
|
||||||
adding the following to the `android` section:
|
|
||||||
|
|
||||||
```gradle
|
|
||||||
compileOptions {
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
As an alternative to the full library, you can depend on only the library
|
As an alternative to the full library, you can depend on only the library
|
||||||
modules that you actually need. For example the following will add dependencies
|
modules that you actually need. For example the following will add dependencies
|
||||||
@ -87,6 +83,32 @@ JCenter can be found on [Bintray][].
|
|||||||
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
|
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
|
||||||
[Bintray]: https://bintray.com/google/exoplayer
|
[Bintray]: https://bintray.com/google/exoplayer
|
||||||
|
|
||||||
|
#### 3. Turn on Java 8 support ####
|
||||||
|
|
||||||
|
If not enabled already, you also need to turn on Java 8 support in all
|
||||||
|
`build.gradle` files depending on ExoPlayer, by adding the following to the
|
||||||
|
`android` section:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
compileOptions {
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that if you want to use Java 8 features in your own code, the following
|
||||||
|
additional options need to be set:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
// For Java compilers:
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
// For Kotlin compilers:
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Locally ###
|
### Locally ###
|
||||||
|
|
||||||
Cloning the repository and depending on the modules locally is required when
|
Cloning the repository and depending on the modules locally is required when
|
||||||
|
@ -5,13 +5,90 @@
|
|||||||
* Support for playing spherical videos on Daydream.
|
* Support for playing spherical videos on Daydream.
|
||||||
* Improve decoder re-use between playbacks. TODO: Write and link a blog post
|
* Improve decoder re-use between playbacks. TODO: Write and link a blog post
|
||||||
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)).
|
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)).
|
||||||
* Add options for controlling audio track selections to `DefaultTrackSelector`
|
* Track selection:
|
||||||
([#3314](https://github.com/google/ExoPlayer/issues/3314)).
|
* 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`.
|
* Do not retry failed loads whose error is `FileNotFoundException`.
|
||||||
* Prevent Cea608Decoder from generating Subtitles with null Cues list
|
* Offline:
|
||||||
* Caching: Cache data with unknown length by default. The previous flag to opt in
|
* Speed up removal of segmented downloads
|
||||||
to this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
|
([#5136](https://github.com/google/ExoPlayer/issues/5136)).
|
||||||
replaced with an opt out flag (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
|
* 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 ###
|
### 2.9.2 ###
|
||||||
|
|
||||||
@ -60,10 +137,10 @@
|
|||||||
* DASH: Parse ProgramInformation element if present in the manifest.
|
* DASH: Parse ProgramInformation element if present in the manifest.
|
||||||
* HLS:
|
* HLS:
|
||||||
* Add constructor to `DefaultHlsExtractorFactory` for adding TS payload
|
* 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
|
* Fix bug in segment sniffing
|
||||||
([#5039](https://github.com/google/ExoPlayer/issues/5039)).
|
([#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
|
* SubRip: Add support for alignment tags, and remove tags from the displayed
|
||||||
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
|
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
|
||||||
* Fix issue with blind seeking to windows with non-zero offset in a
|
* 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).
|
[here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi).
|
||||||
* Robustness improvements when handling MediaSource timeline changes and
|
* Robustness improvements when handling MediaSource timeline changes and
|
||||||
MediaPeriod transitions.
|
MediaPeriod transitions.
|
||||||
* EIA608: Support for caption styling and positioning.
|
* CEA-608: Support for caption styling and positioning.
|
||||||
* MPEG-TS: Improved support:
|
* MPEG-TS: Improved support:
|
||||||
* Support injection of custom TS payload readers.
|
* Support injection of custom TS payload readers.
|
||||||
* Support injection of custom section payload readers.
|
* Support injection of custom section payload readers.
|
||||||
@ -1369,8 +1446,8 @@ V2 release.
|
|||||||
(#801).
|
(#801).
|
||||||
* MP3: Fix playback of some streams when stream length is unknown.
|
* MP3: Fix playback of some streams when stream length is unknown.
|
||||||
* ID3: Support multiple frames of the same type in a single tag.
|
* ID3: Support multiple frames of the same type in a single tag.
|
||||||
* EIA608: Correctly handle repeated control characters, fixing an issue in which
|
* CEA-608: Correctly handle repeated control characters, fixing an issue in
|
||||||
captions would immediately disappear.
|
which captions would immediately disappear.
|
||||||
* AVC3: Fix decoder failures on some MediaTek devices in the case where the
|
* AVC3: Fix decoder failures on some MediaTek devices in the case where the
|
||||||
first buffer fed to the decoder does not start with SPS/PPS NAL units.
|
first buffer fed to the decoder does not start with SPS/PPS NAL units.
|
||||||
* Misc bug fixes.
|
* Misc bug fixes.
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.9.2'
|
releaseVersion = '2.9.4'
|
||||||
releaseVersionCode = 2009002
|
releaseVersionCode = 2009004
|
||||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
||||||
// components provided by the library may be of use on older devices.
|
// components provided by the library may be of use on older devices.
|
||||||
// However, please note that the core media playback functionality provided
|
// However, please note that the core media playback functionality provided
|
||||||
|
@ -49,6 +49,16 @@ android {
|
|||||||
disable 'MissingTranslation'
|
disable 'MissingTranslation'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions "receiver"
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
defaultCast {
|
||||||
|
dimension "receiver"
|
||||||
|
manifestPlaceholders =
|
||||||
|
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
android:largeHeap="true" android:allowBackup="false">
|
android:largeHeap="true" android:allowBackup="false">
|
||||||
|
|
||||||
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
<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"
|
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||||
|
@ -268,7 +268,7 @@ import java.util.ArrayList;
|
|||||||
public void onTimelineChanged(
|
public void onTimelineChanged(
|
||||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
||||||
updateCurrentItemIndex();
|
updateCurrentItemIndex();
|
||||||
if (timeline.isEmpty()) {
|
if (currentPlayer == castPlayer && timeline.isEmpty()) {
|
||||||
castMediaQueueCreationPending = true;
|
castMediaQueueCreationPending = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,59 +15,99 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.castdemo;
|
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 com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/** Utility methods and constants for the Cast demo application. */
|
/** Utility methods and constants for the Cast demo application. */
|
||||||
/* package */ final class DemoUtil {
|
/* package */ final class DemoUtil {
|
||||||
|
|
||||||
|
/** 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_DASH = MimeTypes.APPLICATION_MPD;
|
||||||
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
|
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_SS = MimeTypes.APPLICATION_SS;
|
||||||
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
|
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
|
||||||
|
|
||||||
/** The list of samples available in the cast demo app. */
|
/** The list of samples available in the cast demo app. */
|
||||||
public static final List<MediaItem> SAMPLES;
|
public static final List<Sample> SAMPLES;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// App samples.
|
// App samples.
|
||||||
ArrayList<MediaItem> samples = new ArrayList<>();
|
ArrayList<Sample> samples = new ArrayList<>();
|
||||||
MediaItem.Builder sampleBuilder = new MediaItem.Builder();
|
|
||||||
|
|
||||||
samples.add(
|
samples.add(
|
||||||
sampleBuilder
|
new Sample(
|
||||||
.setTitle("DASH (clear,MP4,H264)")
|
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
||||||
.setMimeType(MIME_TYPE_DASH)
|
"Clear DASH: Tears",
|
||||||
.setMedia("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
|
MIME_TYPE_DASH));
|
||||||
.buildAndClear());
|
|
||||||
|
|
||||||
samples.add(
|
samples.add(
|
||||||
sampleBuilder
|
new Sample(
|
||||||
.setTitle("Tears of Steel (HLS)")
|
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
|
||||||
.setMimeType(MIME_TYPE_HLS)
|
+ "hls/TearsOfSteel.m3u8",
|
||||||
.setMedia(
|
"Clear HLS: Tears of Steel",
|
||||||
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
|
MIME_TYPE_HLS));
|
||||||
+ "hls/TearsOfSteel.m3u8")
|
|
||||||
.buildAndClear());
|
|
||||||
|
|
||||||
samples.add(
|
samples.add(
|
||||||
sampleBuilder
|
new Sample(
|
||||||
.setTitle("HLS Basic (TS)")
|
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3"
|
||||||
.setMimeType(MIME_TYPE_HLS)
|
+ "/bipbop_4x3_variant.m3u8",
|
||||||
.setMedia(
|
"Clear HLS: Basic 4x3",
|
||||||
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3"
|
MIME_TYPE_HLS));
|
||||||
+ "/bipbop_4x3_variant.m3u8")
|
|
||||||
.buildAndClear());
|
|
||||||
|
|
||||||
samples.add(
|
samples.add(
|
||||||
sampleBuilder
|
new Sample(
|
||||||
.setTitle("Dizzy (MP4)")
|
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4));
|
||||||
.setMimeType(MIME_TYPE_VIDEO_MP4)
|
|
||||||
.setMedia("https://html5demos.com/assets/dizzy.mp4")
|
|
||||||
.buildAndClear());
|
|
||||||
SAMPLES = Collections.unmodifiableList(samples);
|
SAMPLES = Collections.unmodifiableList(samples);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer2.castdemo;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.graphics.ColorUtils;
|
import android.support.v4.graphics.ColorUtils;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
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.CastMediaControlIntent;
|
||||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
import com.google.android.gms.cast.framework.CastContext;
|
||||||
|
import com.google.android.gms.dynamite.DynamiteModule;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
|
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
|
||||||
@ -50,6 +51,8 @@ import com.google.android.gms.cast.framework.CastContext;
|
|||||||
public class MainActivity extends AppCompatActivity
|
public class MainActivity extends AppCompatActivity
|
||||||
implements OnClickListener, PlayerManager.QueuePositionListener {
|
implements OnClickListener, PlayerManager.QueuePositionListener {
|
||||||
|
|
||||||
|
private final MediaItem.Builder mediaItemBuilder;
|
||||||
|
|
||||||
private PlayerView localPlayerView;
|
private PlayerView localPlayerView;
|
||||||
private PlayerControlView castControlView;
|
private PlayerControlView castControlView;
|
||||||
private PlayerManager playerManager;
|
private PlayerManager playerManager;
|
||||||
@ -57,13 +60,30 @@ public class MainActivity extends AppCompatActivity
|
|||||||
private MediaQueueListAdapter mediaQueueListAdapter;
|
private MediaQueueListAdapter mediaQueueListAdapter;
|
||||||
private CastContext castContext;
|
private CastContext castContext;
|
||||||
|
|
||||||
|
public MainActivity() {
|
||||||
|
mediaItemBuilder = new MediaItem.Builder();
|
||||||
|
}
|
||||||
|
|
||||||
// Activity lifecycle methods.
|
// Activity lifecycle methods.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
// Getting the cast context later than onStart can cause device discovery not to take place.
|
// 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);
|
setContentView(R.layout.main_activity);
|
||||||
|
|
||||||
@ -93,6 +113,10 @@ public class MainActivity extends AppCompatActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
if (castContext == null) {
|
||||||
|
// There is no Cast context to work with. Do nothing.
|
||||||
|
return;
|
||||||
|
}
|
||||||
String applicationId = castContext.getCastOptions().getReceiverApplicationId();
|
String applicationId = castContext.getCastOptions().getReceiverApplicationId();
|
||||||
switch (applicationId) {
|
switch (applicationId) {
|
||||||
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
|
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
|
||||||
@ -113,6 +137,10 @@ public class MainActivity extends AppCompatActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
if (castContext == null) {
|
||||||
|
// Nothing to release.
|
||||||
|
return;
|
||||||
|
}
|
||||||
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
|
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
|
||||||
mediaQueueList.setAdapter(null);
|
mediaQueueList.setAdapter(null);
|
||||||
playerManager.release();
|
playerManager.release();
|
||||||
@ -154,7 +182,19 @@ public class MainActivity extends AppCompatActivity
|
|||||||
sampleList.setAdapter(new SampleListAdapter(this));
|
sampleList.setAdapter(new SampleListAdapter(this));
|
||||||
sampleList.setOnItemClickListener(
|
sampleList.setOnItemClickListener(
|
||||||
(parent, view, position, id) -> {
|
(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);
|
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
|
||||||
});
|
});
|
||||||
return dialogList;
|
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) {
|
public SampleListAdapter(Context context) {
|
||||||
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
@ -22,4 +22,6 @@
|
|||||||
|
|
||||||
<string name="sample_list_dialog_title">Add samples</string>
|
<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>
|
</resources>
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||||
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
|
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||||
@ -72,6 +74,17 @@ public class DemoApplication extends Application {
|
|||||||
return "withExtensions".equals(BuildConfig.FLAVOR);
|
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() {
|
public DownloadManager getDownloadManager() {
|
||||||
initDownloadManager();
|
initDownloadManager();
|
||||||
return downloadManager;
|
return downloadManager;
|
||||||
@ -88,10 +101,12 @@ public class DemoApplication extends Application {
|
|||||||
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
||||||
downloadManager =
|
downloadManager =
|
||||||
new DownloadManager(
|
new DownloadManager(
|
||||||
|
this,
|
||||||
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
|
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
|
||||||
new DefaultDownloaderFactory(downloaderConstructorHelper),
|
new DefaultDownloaderFactory(downloaderConstructorHelper),
|
||||||
MAX_SIMULTANEOUS_DOWNLOADS,
|
MAX_SIMULTANEOUS_DOWNLOADS,
|
||||||
DownloadManager.DEFAULT_MIN_RETRY_COUNT);
|
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
|
||||||
|
DownloadManager.DEFAULT_REQUIREMENTS);
|
||||||
downloadTracker =
|
downloadTracker =
|
||||||
new DownloadTracker(
|
new DownloadTracker(
|
||||||
/* context= */ this,
|
/* context= */ this,
|
||||||
|
@ -17,8 +17,8 @@ package com.google.android.exoplayer2.demo;
|
|||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
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.DownloadService;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadState;
|
||||||
import com.google.android.exoplayer2.scheduler.PlatformScheduler;
|
import com.google.android.exoplayer2.scheduler.PlatformScheduler;
|
||||||
import com.google.android.exoplayer2.ui.DownloadNotificationUtil;
|
import com.google.android.exoplayer2.ui.DownloadNotificationUtil;
|
||||||
import com.google.android.exoplayer2.util.NotificationUtil;
|
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 JOB_ID = 1;
|
||||||
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
|
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||||
|
|
||||||
public DemoDownloadService() {
|
public DemoDownloadService() {
|
||||||
super(
|
super(
|
||||||
FOREGROUND_NOTIFICATION_ID,
|
FOREGROUND_NOTIFICATION_ID,
|
||||||
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
R.string.exo_download_notification_channel_name);
|
R.string.exo_download_notification_channel_name);
|
||||||
|
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -50,40 +53,38 @@ public class DemoDownloadService extends DownloadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Notification getForegroundNotification(TaskState[] taskStates) {
|
protected Notification getForegroundNotification(DownloadState[] downloadStates) {
|
||||||
return DownloadNotificationUtil.buildProgressNotification(
|
return DownloadNotificationUtil.buildProgressNotification(
|
||||||
/* context= */ this,
|
/* context= */ this,
|
||||||
R.drawable.exo_controls_play,
|
R.drawable.ic_download,
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
/* contentIntent= */ null,
|
/* contentIntent= */ null,
|
||||||
/* message= */ null,
|
/* message= */ null,
|
||||||
taskStates);
|
downloadStates);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onTaskStateChanged(TaskState taskState) {
|
protected void onDownloadStateChanged(DownloadState downloadState) {
|
||||||
if (taskState.action.isRemoveAction) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Notification notification = null;
|
Notification notification = null;
|
||||||
if (taskState.state == TaskState.STATE_COMPLETED) {
|
if (downloadState.state == DownloadState.STATE_COMPLETED) {
|
||||||
notification =
|
notification =
|
||||||
DownloadNotificationUtil.buildDownloadCompletedNotification(
|
DownloadNotificationUtil.buildDownloadCompletedNotification(
|
||||||
/* context= */ this,
|
/* context= */ this,
|
||||||
R.drawable.exo_controls_play,
|
R.drawable.ic_download_done,
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
/* contentIntent= */ null,
|
/* contentIntent= */ null,
|
||||||
Util.fromUtf8Bytes(taskState.action.data));
|
Util.fromUtf8Bytes(downloadState.customMetadata));
|
||||||
} else if (taskState.state == TaskState.STATE_FAILED) {
|
} else if (downloadState.state == DownloadState.STATE_FAILED) {
|
||||||
notification =
|
notification =
|
||||||
DownloadNotificationUtil.buildDownloadFailedNotification(
|
DownloadNotificationUtil.buildDownloadFailedNotification(
|
||||||
/* context= */ this,
|
/* context= */ this,
|
||||||
R.drawable.exo_controls_play,
|
R.drawable.ic_download_done,
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
/* contentIntent= */ null,
|
/* contentIntent= */ null,
|
||||||
Util.fromUtf8Bytes(taskState.action.data));
|
Util.fromUtf8Bytes(downloadState.customMetadata));
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId;
|
NotificationUtil.setNotification(this, nextNotificationId++, notification);
|
||||||
NotificationUtil.setNotification(this, notificationId, notification);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,37 +19,44 @@ import android.app.Activity;
|
|||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ListView;
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.offline.ActionFile;
|
import com.google.android.exoplayer2.offline.ActionFile;
|
||||||
import com.google.android.exoplayer2.offline.DownloadAction;
|
import com.google.android.exoplayer2.offline.DownloadAction;
|
||||||
import com.google.android.exoplayer2.offline.DownloadHelper;
|
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
|
|
||||||
import com.google.android.exoplayer2.offline.DownloadService;
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadState;
|
||||||
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
|
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
|
||||||
import com.google.android.exoplayer2.offline.StreamKey;
|
import com.google.android.exoplayer2.offline.StreamKey;
|
||||||
import com.google.android.exoplayer2.offline.TrackKey;
|
import com.google.android.exoplayer2.scheduler.Requirements;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper;
|
import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper;
|
||||||
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper;
|
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper;
|
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.DefaultTrackNameProvider;
|
||||||
import com.google.android.exoplayer2.ui.TrackNameProvider;
|
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.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -114,14 +121,19 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
return trackedDownloadStates.get(uri).getKeys();
|
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)) {
|
if (isDownloaded(uri)) {
|
||||||
DownloadAction removeAction = getDownloadHelper(uri, extension).getRemoveAction();
|
DownloadAction removeAction =
|
||||||
|
getDownloadHelper(uri, extension, renderersFactory).getRemoveAction();
|
||||||
startServiceWithAction(removeAction);
|
startServiceWithAction(removeAction);
|
||||||
} else {
|
} else {
|
||||||
StartDownloadDialogHelper helper =
|
new StartDownloadDialogHelper(
|
||||||
new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name);
|
activity, getDownloadHelper(uri, extension, renderersFactory), name);
|
||||||
helper.prepare();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,13 +145,11 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
|
public void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState) {
|
||||||
DownloadAction action = taskState.action;
|
if (downloadState.state == DownloadState.STATE_REMOVED
|
||||||
Uri uri = action.uri;
|
|| downloadState.state == DownloadState.STATE_FAILED) {
|
||||||
if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED)
|
|
||||||
|| (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) {
|
|
||||||
// A download has been removed, or has failed. Stop tracking it.
|
// A download has been removed, or has failed. Stop tracking it.
|
||||||
if (trackedDownloadStates.remove(uri) != null) {
|
if (trackedDownloadStates.remove(downloadState.uri) != null) {
|
||||||
handleTrackedDownloadStatesChanged();
|
handleTrackedDownloadStatesChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,6 +160,14 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequirementsStateChanged(
|
||||||
|
DownloadManager downloadManager,
|
||||||
|
Requirements requirements,
|
||||||
|
@Requirements.RequirementFlags int notMetRequirements) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods
|
// Internal methods
|
||||||
|
|
||||||
private void loadTrackedActions() {
|
private void loadTrackedActions() {
|
||||||
@ -192,15 +210,16 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
|
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);
|
int type = Util.inferContentType(uri, extension);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return new DashDownloadHelper(uri, dataSourceFactory);
|
return new DashDownloadHelper(uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsDownloadHelper(uri, dataSourceFactory);
|
return new SsDownloadHelper(uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsDownloadHelper(uri, dataSourceFactory);
|
return new HlsDownloadHelper(uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ProgressiveDownloadHelper(uri);
|
return new ProgressiveDownloadHelper(uri);
|
||||||
default:
|
default:
|
||||||
@ -208,84 +227,165 @@ public class DownloadTracker implements DownloadManager.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("UngroupedOverloads")
|
||||||
private final class StartDownloadDialogHelper
|
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 String name;
|
||||||
|
private final LayoutInflater dialogInflater;
|
||||||
|
private final AlertDialog dialog;
|
||||||
|
private final LinearLayout selectionList;
|
||||||
|
|
||||||
private final AlertDialog.Builder builder;
|
private MappedTrackInfo mappedTrackInfo;
|
||||||
private final View dialogView;
|
private DefaultTrackSelector.Parameters parameters;
|
||||||
private final List<TrackKey> trackKeys;
|
|
||||||
private final ArrayAdapter<String> trackTitles;
|
|
||||||
private final ListView representationList;
|
|
||||||
|
|
||||||
public StartDownloadDialogHelper(
|
private StartDownloadDialogHelper(
|
||||||
Activity activity, DownloadHelper downloadHelper, String name) {
|
Activity activity, DownloadHelper<?> downloadHelper, String name) {
|
||||||
this.downloadHelper = downloadHelper;
|
this.downloadHelper = downloadHelper;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
builder =
|
AlertDialog.Builder builder =
|
||||||
new AlertDialog.Builder(activity)
|
new AlertDialog.Builder(activity)
|
||||||
.setTitle(R.string.exo_download_description)
|
.setTitle(R.string.download_preparing)
|
||||||
.setPositiveButton(android.R.string.ok, this)
|
.setPositiveButton(android.R.string.ok, this)
|
||||||
.setNegativeButton(android.R.string.cancel, null);
|
.setNegativeButton(android.R.string.cancel, null);
|
||||||
|
|
||||||
// Inflate with the builder's context to ensure the correct style is used.
|
// Inflate with the builder's context to ensure the correct style is used.
|
||||||
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
|
dialogInflater = LayoutInflater.from(builder.getContext());
|
||||||
dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null);
|
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<>();
|
parameters = DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS;
|
||||||
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() {
|
|
||||||
downloadHelper.prepare(this);
|
downloadHelper.prepare(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DownloadHelper.Callback implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepared(DownloadHelper helper) {
|
public void onPrepared(DownloadHelper<?> helper) {
|
||||||
for (int i = 0; i < downloadHelper.getPeriodCount(); i++) {
|
if (helper.getPeriodCount() < 1) {
|
||||||
TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i);
|
onPrepareError(downloadHelper, new IOException("Content is empty."));
|
||||||
for (int j = 0; j < trackGroups.length; j++) {
|
return;
|
||||||
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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!trackKeys.isEmpty()) {
|
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||||
builder.setView(dialogView);
|
updateSelectionList();
|
||||||
}
|
dialog.setTitle(R.string.exo_download_description);
|
||||||
builder.create().show();
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareError(DownloadHelper helper, IOException e) {
|
public void onPrepareError(DownloadHelper<?> helper, IOException e) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
|
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
|
||||||
.show();
|
.show();
|
||||||
Log.e(TAG, "Failed to start download", e);
|
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
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
ArrayList<TrackKey> selectedTrackKeys = new ArrayList<>();
|
DownloadAction downloadAction = downloadHelper.getDownloadAction(Util.getUtf8Bytes(name));
|
||||||
for (int i = 0; i < representationList.getChildCount(); i++) {
|
startDownload(downloadAction);
|
||||||
if (representationList.isItemChecked(i)) {
|
}
|
||||||
selectedTrackKeys.add(trackKeys.get(i));
|
|
||||||
|
// 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()) {
|
return selectedTracks.isEmpty()
|
||||||
// We have selected keys, or we're dealing with single stream content.
|
? resources.getString(R.string.exo_track_selection_none)
|
||||||
DownloadAction downloadAction =
|
: selectedTracks;
|
||||||
downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys);
|
}
|
||||||
startDownload(downloadAction);
|
|
||||||
|
@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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,11 +35,11 @@ import android.widget.TextView;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.C.ContentType;
|
import com.google.android.exoplayer2.C.ContentType;
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
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.drm.UnsupportedDrmException;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
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.offline.StreamKey;
|
||||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
@ -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.AdsLoader;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
|
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
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.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
|
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
@ -416,13 +412,8 @@ public class PlayerActivity extends Activity
|
|||||||
|
|
||||||
boolean preferExtensionDecoders =
|
boolean preferExtensionDecoders =
|
||||||
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
|
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
|
RenderersFactory renderersFactory =
|
||||||
((DemoApplication) getApplication()).useExtensionRenderers()
|
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
|
||||||
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
|
||||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
|
||||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
|
||||||
DefaultRenderersFactory renderersFactory =
|
|
||||||
new DefaultRenderersFactory(this, extensionRendererMode);
|
|
||||||
|
|
||||||
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
||||||
trackSelector.setParameters(trackSelectorParameters);
|
trackSelector.setParameters(trackSelectorParameters);
|
||||||
@ -477,21 +468,19 @@ public class PlayerActivity extends Activity
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
||||||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
||||||
|
List<StreamKey> offlineStreamKeys = getOfflineStreamKeys(uri);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return new DashMediaSource.Factory(dataSourceFactory)
|
return new DashMediaSource.Factory(dataSourceFactory)
|
||||||
.setManifestParser(
|
.setStreamKeys(offlineStreamKeys)
|
||||||
new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsMediaSource.Factory(dataSourceFactory)
|
return new SsMediaSource.Factory(dataSourceFactory)
|
||||||
.setManifestParser(
|
.setStreamKeys(offlineStreamKeys)
|
||||||
new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(dataSourceFactory)
|
return new HlsMediaSource.Factory(dataSourceFactory)
|
||||||
.setPlaylistParserFactory(
|
.setStreamKeys(offlineStreamKeys)
|
||||||
new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
@ -534,6 +523,9 @@ public class PlayerActivity extends Activity
|
|||||||
mediaSource = null;
|
mediaSource = null;
|
||||||
trackSelector = null;
|
trackSelector = null;
|
||||||
}
|
}
|
||||||
|
if (adsLoader != null) {
|
||||||
|
adsLoader.setPlayer(null);
|
||||||
|
}
|
||||||
releaseMediaDrm();
|
releaseMediaDrm();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -597,6 +589,7 @@ public class PlayerActivity extends Activity
|
|||||||
// The demo app has a non-null overlay frame layout.
|
// The demo app has a non-null overlay frame layout.
|
||||||
playerView.getOverlayFrameLayout().addView(adUiViewGroup);
|
playerView.getOverlayFrameLayout().addView(adUiViewGroup);
|
||||||
}
|
}
|
||||||
|
adsLoader.setPlayer(player);
|
||||||
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
|
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
|
||||||
new AdsMediaSource.MediaSourceFactory() {
|
new AdsMediaSource.MediaSourceFactory() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -37,6 +37,7 @@ import android.widget.ImageButton;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.offline.DownloadService;
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
||||||
@ -177,7 +178,11 @@ public class SampleChooserActivity extends Activity
|
|||||||
.show();
|
.show();
|
||||||
} else {
|
} else {
|
||||||
UriSample uriSample = (UriSample) sample;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
demos/main/src/main/res/drawable-hdpi/ic_edit.png
Executable file
BIN
demos/main/src/main/res/drawable-hdpi/ic_edit.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 304 B |
BIN
demos/main/src/main/res/drawable-mdpi/ic_edit.png
Executable file
BIN
demos/main/src/main/res/drawable-mdpi/ic_edit.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 242 B |
BIN
demos/main/src/main/res/drawable-xhdpi/ic_edit.png
Executable file
BIN
demos/main/src/main/res/drawable-xhdpi/ic_edit.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 299 B |
BIN
demos/main/src/main/res/drawable-xxhdpi/ic_edit.png
Executable file
BIN
demos/main/src/main/res/drawable-xxhdpi/ic_edit.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 413 B |
BIN
demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png
Executable file
BIN
demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 449 B |
53
demos/main/src/main/res/layout/download_track_item.xml
Normal file
53
demos/main/src/main/res/layout/download_track_item.xml
Normal 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>
|
@ -13,7 +13,8 @@
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/representation_list"
|
android:id="@+id/selection_list"
|
||||||
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"/>
|
||||||
|
@ -51,6 +51,10 @@
|
|||||||
|
|
||||||
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
|
<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_start_error">Failed to start download</string>
|
||||||
|
|
||||||
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
|
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
|
||||||
|
@ -31,7 +31,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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-qual:' + checkerframeworkVersion
|
||||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
|
@ -266,20 +266,29 @@ public final class CastPlayer extends BasePlayer {
|
|||||||
// Player implementation.
|
// Player implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public AudioComponent getAudioComponent() {
|
public AudioComponent getAudioComponent() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public VideoComponent getVideoComponent() {
|
public VideoComponent getVideoComponent() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public TextComponent getTextComponent() {
|
public TextComponent getTextComponent() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public MetadataComponent getMetadataComponent() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Looper getApplicationLooper() {
|
public Looper getApplicationLooper() {
|
||||||
return Looper.getMainLooper();
|
return Looper.getMainLooper();
|
||||||
|
@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
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.BaseDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceException;
|
import com.google.android.exoplayer2.upstream.DataSourceException;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
@ -493,6 +494,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
|
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
|
||||||
throw new IOException("HTTP request with non-empty body must set Content-Type");
|
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.
|
// Set the Range header.
|
||||||
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
||||||
StringBuilder rangeValue = new StringBuilder();
|
StringBuilder rangeValue = new StringBuilder();
|
||||||
|
@ -37,6 +37,10 @@ import java.util.List;
|
|||||||
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
|
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
|
||||||
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
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 String codecName;
|
||||||
private final @Nullable byte[] extraData;
|
private final @Nullable byte[] extraData;
|
||||||
private final @C.Encoding int encoding;
|
private final @C.Encoding int encoding;
|
||||||
@ -106,8 +110,14 @@ import java.util.List;
|
|||||||
int inputSize = inputData.limit();
|
int inputSize = inputData.limit();
|
||||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
|
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
|
||||||
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
|
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
|
||||||
if (result < 0) {
|
if (result == DECODER_ERROR_INVALID_DATA) {
|
||||||
return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result);
|
// 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) {
|
if (!hasOutputFormat) {
|
||||||
channelCount = ffmpegGetChannelCount(nativeContext);
|
channelCount = ffmpegGetChannelCount(nativeContext);
|
||||||
|
@ -63,6 +63,10 @@ static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16;
|
|||||||
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
|
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
|
||||||
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
|
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.
|
* 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
|
* 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,
|
int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||||
uint8_t *outputBuffer, int outputSize);
|
uint8_t *outputBuffer, int outputSize);
|
||||||
@ -238,6 +242,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
|
|||||||
context->channels = rawChannelCount;
|
context->channels = rawChannelCount;
|
||||||
context->channel_layout = av_get_default_channel_layout(rawChannelCount);
|
context->channel_layout = av_get_default_channel_layout(rawChannelCount);
|
||||||
}
|
}
|
||||||
|
context->err_recognition = AV_EF_IGNORE_ERR;
|
||||||
int result = avcodec_open2(context, codec, NULL);
|
int result = avcodec_open2(context, codec, NULL);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
logError("avcodec_open2", result);
|
logError("avcodec_open2", result);
|
||||||
@ -254,7 +259,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
|||||||
result = avcodec_send_packet(context, packet);
|
result = avcodec_send_packet(context, packet);
|
||||||
if (result) {
|
if (result) {
|
||||||
logError("avcodec_send_packet", 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.
|
// Dequeue output data until it runs out.
|
||||||
|
@ -33,9 +33,7 @@ dependencies {
|
|||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation project(modulePrefix + 'library-ui')
|
implementation project(modulePrefix + 'library-ui')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||||
implementation 'com.google.vr:sdk-audio:1.80.0'
|
api 'com.google.vr:sdk-base:1.190.0'
|
||||||
implementation 'com.google.vr:sdk-controller:1.80.0'
|
|
||||||
api 'com.google.vr:sdk-base:1.80.0'
|
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
@ -74,7 +73,13 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
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
|
public final class ImaAdsLoader
|
||||||
implements Player.EventListener,
|
implements Player.EventListener,
|
||||||
AdsLoader,
|
AdsLoader,
|
||||||
@ -93,9 +98,9 @@ public final class ImaAdsLoader
|
|||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
private @Nullable ImaSdkSettings imaSdkSettings;
|
@Nullable private ImaSdkSettings imaSdkSettings;
|
||||||
private @Nullable AdEventListener adEventListener;
|
@Nullable private AdEventListener adEventListener;
|
||||||
private @Nullable Set<UiElement> adUiElements;
|
@Nullable private Set<UiElement> adUiElements;
|
||||||
private int vastLoadTimeoutMs;
|
private int vastLoadTimeoutMs;
|
||||||
private int mediaLoadTimeoutMs;
|
private int mediaLoadTimeoutMs;
|
||||||
private int mediaBitrate;
|
private int mediaBitrate;
|
||||||
@ -317,10 +322,11 @@ public final class ImaAdsLoader
|
|||||||
private final AdDisplayContainer adDisplayContainer;
|
private final AdDisplayContainer adDisplayContainer;
|
||||||
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
|
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
|
||||||
|
|
||||||
|
@Nullable private Player nextPlayer;
|
||||||
private Object pendingAdRequestContext;
|
private Object pendingAdRequestContext;
|
||||||
private List<String> supportedMimeTypes;
|
private List<String> supportedMimeTypes;
|
||||||
private EventListener eventListener;
|
@Nullable private EventListener eventListener;
|
||||||
private Player player;
|
@Nullable private Player player;
|
||||||
private VideoProgressUpdate lastContentProgress;
|
private VideoProgressUpdate lastContentProgress;
|
||||||
private VideoProgressUpdate lastAdProgress;
|
private VideoProgressUpdate lastAdProgress;
|
||||||
private int lastVolumePercentage;
|
private int lastVolumePercentage;
|
||||||
@ -526,6 +532,14 @@ public final class ImaAdsLoader
|
|||||||
|
|
||||||
// AdsLoader implementation.
|
// 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
|
@Override
|
||||||
public void setSupportedContentTypes(@C.ContentType int... contentTypes) {
|
public void setSupportedContentTypes(@C.ContentType int... contentTypes) {
|
||||||
List<String> supportedMimeTypes = new ArrayList<>();
|
List<String> supportedMimeTypes = new ArrayList<>();
|
||||||
@ -550,9 +564,10 @@ public final class ImaAdsLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
|
public void start(EventListener eventListener, ViewGroup adUiViewGroup) {
|
||||||
Assertions.checkArgument(player.getApplicationLooper() == Looper.getMainLooper());
|
Assertions.checkNotNull(
|
||||||
this.player = player;
|
nextPlayer, "Set player using adsLoader.setPlayer before preparing the player.");
|
||||||
|
player = nextPlayer;
|
||||||
this.eventListener = eventListener;
|
this.eventListener = eventListener;
|
||||||
lastVolumePercentage = 0;
|
lastVolumePercentage = 0;
|
||||||
lastAdProgress = null;
|
lastAdProgress = null;
|
||||||
@ -576,7 +591,7 @@ public final class ImaAdsLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void detachPlayer() {
|
public void stop() {
|
||||||
if (adsManager != null && imaPausedContent) {
|
if (adsManager != null && imaPausedContent) {
|
||||||
adPlaybackState =
|
adPlaybackState =
|
||||||
adPlaybackState.withAdResumePositionUs(
|
adPlaybackState.withAdResumePositionUs(
|
||||||
@ -598,6 +613,8 @@ public final class ImaAdsLoader
|
|||||||
adsManager.destroy();
|
adsManager.destroy();
|
||||||
adsManager = null;
|
adsManager = null;
|
||||||
}
|
}
|
||||||
|
adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this);
|
||||||
|
adsLoader.removeAdErrorListener(/* adErrorListener= */ this);
|
||||||
imaPausedContent = false;
|
imaPausedContent = false;
|
||||||
imaAdState = IMA_AD_STATE_NONE;
|
imaAdState = IMA_AD_STATE_NONE;
|
||||||
pendingAdLoadError = null;
|
pendingAdLoadError = null;
|
||||||
|
@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.ima;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.source.BaseMediaSource;
|
import com.google.android.exoplayer2.source.BaseMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
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.
|
* 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
|
@Deprecated
|
||||||
public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener {
|
public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener {
|
||||||
@ -83,12 +83,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepareSourceInternal(
|
public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
final ExoPlayer player,
|
adsMediaSource.prepareSource(/* listener= */ this, mediaTransferListener);
|
||||||
boolean isTopLevelSource,
|
|
||||||
@Nullable TransferListener mediaTransferListener) {
|
|
||||||
adsMediaSource.prepareSource(
|
|
||||||
player, isTopLevelSource, /* listener= */ this, mediaTransferListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -97,8 +93,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||||
return adsMediaSource.createPeriod(id, allocator);
|
return adsMediaSource.createPeriod(id, allocator, startPositionUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -64,14 +64,17 @@ import java.util.Set;
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getVastMediaWidth() {
|
public int getVastMediaWidth() {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getVastMediaHeight() {
|
public int getVastMediaHeight() {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getVastMediaBitrate() {
|
public int getVastMediaBitrate() {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ public class ImaAdsLoaderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testAttachPlayer_setsAdUiViewGroup() {
|
public void testAttachPlayer_setsAdUiViewGroup() {
|
||||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
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);
|
verify(adDisplayContainer, atLeastOnce()).setAdContainer(adUiViewGroup);
|
||||||
}
|
}
|
||||||
@ -119,7 +119,7 @@ public class ImaAdsLoaderTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testAttachPlayer_updatesAdPlaybackState() {
|
public void testAttachPlayer_updatesAdPlaybackState() {
|
||||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
|
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
|
||||||
|
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
@ -131,14 +131,14 @@ public class ImaAdsLoaderTest {
|
|||||||
public void testAttachAfterRelease() {
|
public void testAttachAfterRelease() {
|
||||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
imaAdsLoader.release();
|
imaAdsLoader.release();
|
||||||
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
|
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAttachAndCallbacksAfterRelease() {
|
public void testAttachAndCallbacksAfterRelease() {
|
||||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
imaAdsLoader.release();
|
imaAdsLoader.release();
|
||||||
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
|
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
|
||||||
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
|
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
|
||||||
fakeExoPlayer.setState(Player.STATE_READY, true);
|
fakeExoPlayer.setState(Player.STATE_READY, true);
|
||||||
|
|
||||||
@ -166,7 +166,7 @@ public class ImaAdsLoaderTest {
|
|||||||
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
|
||||||
|
|
||||||
// Load the preroll ad.
|
// Load the preroll ad.
|
||||||
imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
|
imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
|
||||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
|
||||||
imaAdsLoader.loadAd(TEST_URI.toString());
|
imaAdsLoader.loadAd(TEST_URI.toString());
|
||||||
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
|
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
|
||||||
@ -210,6 +210,7 @@ public class ImaAdsLoaderTest {
|
|||||||
.setImaFactory(testImaFactory)
|
.setImaFactory(testImaFactory)
|
||||||
.setImaSdkSettings(imaSdkSettings)
|
.setImaSdkSettings(imaSdkSettings)
|
||||||
.buildForAdTag(TEST_URI);
|
.buildForAdTag(TEST_URI);
|
||||||
|
imaAdsLoader.setPlayer(fakeExoPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) {
|
private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) {
|
||||||
|
@ -129,7 +129,7 @@ public final class JobDispatcherScheduler implements Scheduler {
|
|||||||
Bundle extras = new Bundle();
|
Bundle extras = new Bundle();
|
||||||
extras.putString(KEY_SERVICE_ACTION, serviceAction);
|
extras.putString(KEY_SERVICE_ACTION, serviceAction);
|
||||||
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
|
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
|
||||||
extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
|
extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
|
||||||
builder.setExtras(extras);
|
builder.setExtras(extras);
|
||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
|
@ -67,10 +67,10 @@ import java.util.Map;
|
|||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code
|
* <li>Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code
|
||||||
* PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
|
* PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed to
|
||||||
* when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
|
* {@link #setPlaybackPreparer(PlaybackPreparer)}.
|
||||||
* actions can be handled by passing one or more {@link CustomActionProvider}s in a similar
|
* <li>Custom actions can be handled by passing one or more {@link CustomActionProvider}s to
|
||||||
* way.
|
* {@link #setCustomActionProviders(CustomActionProvider...)}.
|
||||||
* <li>To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by
|
* <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}
|
* calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator}
|
||||||
* is recommended for most use cases.
|
* is recommended for most use cases.
|
||||||
@ -339,21 +339,21 @@ public final class MediaSessionConnector {
|
|||||||
/** The wrapped {@link MediaSessionCompat}. */
|
/** The wrapped {@link MediaSessionCompat}. */
|
||||||
public final MediaSessionCompat mediaSession;
|
public final MediaSessionCompat mediaSession;
|
||||||
|
|
||||||
@Nullable private final MediaMetadataProvider mediaMetadataProvider;
|
private final Looper looper;
|
||||||
private final ExoPlayerEventListener exoPlayerEventListener;
|
private final ComponentListener componentListener;
|
||||||
private final MediaSessionCallback mediaSessionCallback;
|
|
||||||
private final ArrayList<CommandReceiver> commandReceivers;
|
private final ArrayList<CommandReceiver> commandReceivers;
|
||||||
|
|
||||||
private Player player;
|
|
||||||
private ControlDispatcher controlDispatcher;
|
private ControlDispatcher controlDispatcher;
|
||||||
private CustomActionProvider[] customActionProviders;
|
private CustomActionProvider[] customActionProviders;
|
||||||
private Map<String, CustomActionProvider> customActionMap;
|
private Map<String, CustomActionProvider> customActionMap;
|
||||||
|
@Nullable private MediaMetadataProvider mediaMetadataProvider;
|
||||||
|
@Nullable private Player player;
|
||||||
@Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
@Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
|
||||||
@Nullable private Pair<Integer, CharSequence> customError;
|
@Nullable private Pair<Integer, CharSequence> customError;
|
||||||
private PlaybackPreparer playbackPreparer;
|
@Nullable private PlaybackPreparer playbackPreparer;
|
||||||
private QueueNavigator queueNavigator;
|
@Nullable private QueueNavigator queueNavigator;
|
||||||
private QueueEditor queueEditor;
|
@Nullable private QueueEditor queueEditor;
|
||||||
private RatingCallback ratingCallback;
|
@Nullable private RatingCallback ratingCallback;
|
||||||
|
|
||||||
private long enabledPlaybackActions;
|
private long enabledPlaybackActions;
|
||||||
private int rewindMs;
|
private int rewindMs;
|
||||||
@ -362,82 +362,60 @@ public final class MediaSessionConnector {
|
|||||||
/**
|
/**
|
||||||
* Creates an instance.
|
* Creates an instance.
|
||||||
*
|
*
|
||||||
* <p>Equivalent to {@code MediaSessionConnector(mediaSession, new
|
|
||||||
* DefaultMediaMetadataProvider(mediaSession.getController(), null))}.
|
|
||||||
*
|
|
||||||
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
||||||
*/
|
*/
|
||||||
public MediaSessionConnector(MediaSessionCompat mediaSession) {
|
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.mediaSession = mediaSession;
|
||||||
this.mediaMetadataProvider = mediaMetadataProvider;
|
looper = Util.getLooper();
|
||||||
mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
|
componentListener = new ComponentListener();
|
||||||
mediaSessionCallback = new MediaSessionCallback();
|
|
||||||
exoPlayerEventListener = new ExoPlayerEventListener();
|
|
||||||
controlDispatcher = new DefaultControlDispatcher();
|
|
||||||
customActionMap = Collections.emptyMap();
|
|
||||||
commandReceivers = new ArrayList<>();
|
commandReceivers = new ArrayList<>();
|
||||||
|
controlDispatcher = new DefaultControlDispatcher();
|
||||||
|
customActionProviders = new CustomActionProvider[0];
|
||||||
|
customActionMap = Collections.emptyMap();
|
||||||
|
mediaMetadataProvider =
|
||||||
|
new DefaultMediaMetadataProvider(
|
||||||
|
mediaSession.getController(), /* metadataExtrasPrefix= */ null);
|
||||||
enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS;
|
enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS;
|
||||||
rewindMs = DEFAULT_REWIND_MS;
|
rewindMs = DEFAULT_REWIND_MS;
|
||||||
fastForwardMs = DEFAULT_FAST_FORWARD_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
|
* Sets the player to be connected to the media session. Must be called on the same thread that is
|
||||||
* used to access the player.
|
* 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
|
* @param player The player to be connected to the {@code MediaSession}, or {@code null} to
|
||||||
* disconnect the current player.
|
* 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(
|
public void setPlayer(@Nullable Player player) {
|
||||||
@Nullable Player player,
|
Assertions.checkArgument(player == null || player.getApplicationLooper() == looper);
|
||||||
@Nullable PlaybackPreparer playbackPreparer,
|
|
||||||
CustomActionProvider... customActionProviders) {
|
|
||||||
Assertions.checkArgument(player == null || player.getApplicationLooper() == Looper.myLooper());
|
|
||||||
if (this.player != null) {
|
if (this.player != null) {
|
||||||
this.player.removeListener(exoPlayerEventListener);
|
this.player.removeListener(componentListener);
|
||||||
mediaSession.setCallback(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unregisterCommandReceiver(this.playbackPreparer);
|
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.playbackPreparer = playbackPreparer;
|
|
||||||
registerCommandReceiver(playbackPreparer);
|
|
||||||
|
|
||||||
this.customActionProviders =
|
|
||||||
(player != null && customActionProviders != null)
|
|
||||||
? customActionProviders
|
|
||||||
: new CustomActionProvider[0];
|
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
Handler handler = new Handler(Util.getLooper());
|
player.addListener(componentListener);
|
||||||
mediaSession.setCallback(mediaSessionCallback, handler);
|
|
||||||
player.addListener(exoPlayerEventListener);
|
|
||||||
}
|
}
|
||||||
invalidateMediaSessionPlaybackState();
|
invalidateMediaSessionPlaybackState();
|
||||||
invalidateMediaSessionMetadata();
|
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}.
|
* Sets the {@link ControlDispatcher}.
|
||||||
*
|
*
|
||||||
@ -570,6 +548,32 @@ public final class MediaSessionConnector {
|
|||||||
invalidateMediaSessionPlaybackState();
|
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.
|
* Updates the metadata of the media session.
|
||||||
*
|
*
|
||||||
@ -577,9 +581,11 @@ public final class MediaSessionConnector {
|
|||||||
* changed and the metadata should be updated immediately.
|
* changed and the metadata should be updated immediately.
|
||||||
*/
|
*/
|
||||||
public final void invalidateMediaSessionMetadata() {
|
public final void invalidateMediaSessionMetadata() {
|
||||||
if (mediaMetadataProvider != null && player != null) {
|
MediaMetadataCompat metadata =
|
||||||
mediaSession.setMetadata(mediaMetadataProvider.getMetadata(player));
|
mediaMetadataProvider != null && player != null
|
||||||
}
|
? mediaMetadataProvider.getMetadata(player)
|
||||||
|
: null;
|
||||||
|
mediaSession.setMetadata(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -591,7 +597,7 @@ public final class MediaSessionConnector {
|
|||||||
public final void invalidateMediaSessionPlaybackState() {
|
public final void invalidateMediaSessionPlaybackState() {
|
||||||
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
|
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
|
||||||
if (player == null) {
|
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());
|
mediaSession.setPlaybackState(builder.build());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -627,7 +633,7 @@ public final class MediaSessionConnector {
|
|||||||
Bundle extras = new Bundle();
|
Bundle extras = new Bundle();
|
||||||
extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
|
extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
|
||||||
builder
|
builder
|
||||||
.setActions(buildPlaybackActions(player))
|
.setActions(buildPrepareActions() | buildPlaybackActions(player))
|
||||||
.setActiveQueueItemId(activeQueueItemId)
|
.setActiveQueueItemId(activeQueueItemId)
|
||||||
.setBufferedPosition(player.getBufferedPosition())
|
.setBufferedPosition(player.getBufferedPosition())
|
||||||
.setState(
|
.setState(
|
||||||
@ -662,6 +668,12 @@ public final class MediaSessionConnector {
|
|||||||
commandReceivers.remove(commandReceiver);
|
commandReceivers.remove(commandReceiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long buildPrepareActions() {
|
||||||
|
return playbackPreparer == null
|
||||||
|
? 0
|
||||||
|
: (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
|
||||||
|
}
|
||||||
|
|
||||||
private long buildPlaybackActions(Player player) {
|
private long buildPlaybackActions(Player player) {
|
||||||
boolean enableSeeking = false;
|
boolean enableSeeking = false;
|
||||||
boolean enableRewind = false;
|
boolean enableRewind = false;
|
||||||
@ -688,9 +700,6 @@ public final class MediaSessionConnector {
|
|||||||
playbackActions &= enabledPlaybackActions;
|
playbackActions &= enabledPlaybackActions;
|
||||||
|
|
||||||
long actions = playbackActions;
|
long actions = playbackActions;
|
||||||
if (playbackPreparer != null) {
|
|
||||||
actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
|
|
||||||
}
|
|
||||||
if (queueNavigator != null) {
|
if (queueNavigator != null) {
|
||||||
actions |=
|
actions |=
|
||||||
(QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
|
(QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
|
||||||
@ -719,8 +728,7 @@ public final class MediaSessionConnector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean canDispatchToPlaybackPreparer(long action) {
|
private boolean canDispatchToPlaybackPreparer(long action) {
|
||||||
return player != null
|
return playbackPreparer != null
|
||||||
&& playbackPreparer != null
|
|
||||||
&& (playbackPreparer.getSupportedPrepareActions() & action) != 0;
|
&& (playbackPreparer.getSupportedPrepareActions() & action) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -738,6 +746,13 @@ public final class MediaSessionConnector {
|
|||||||
return player != null && queueEditor != null;
|
return player != null && queueEditor != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void stopPlayerForPrepare(boolean playWhenReady) {
|
||||||
|
if (player != null) {
|
||||||
|
player.stop();
|
||||||
|
player.setPlayWhenReady(playWhenReady);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void rewind(Player player) {
|
private void rewind(Player player) {
|
||||||
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
|
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
|
||||||
seekTo(player, player.getCurrentPosition() - rewindMs);
|
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 currentWindowIndex;
|
||||||
private int currentWindowCount;
|
private int currentWindowCount;
|
||||||
|
|
||||||
|
// Player.EventListener implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(
|
public void onTimelineChanged(
|
||||||
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
|
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
|
||||||
@ -932,9 +950,8 @@ public final class MediaSessionConnector {
|
|||||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||||
invalidateMediaSessionPlaybackState();
|
invalidateMediaSessionPlaybackState();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private class MediaSessionCallback extends MediaSessionCompat.Callback {
|
// MediaSessionCompat.Callback implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlay() {
|
public void onPlay() {
|
||||||
@ -1058,8 +1075,7 @@ public final class MediaSessionConnector {
|
|||||||
@Override
|
@Override
|
||||||
public void onPrepare() {
|
public void onPrepare() {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
|
||||||
player.stop();
|
stopPlayerForPrepare(/* playWhenReady= */ false);
|
||||||
player.setPlayWhenReady(false);
|
|
||||||
playbackPreparer.onPrepare();
|
playbackPreparer.onPrepare();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1067,8 +1083,7 @@ public final class MediaSessionConnector {
|
|||||||
@Override
|
@Override
|
||||||
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
|
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
|
||||||
player.stop();
|
stopPlayerForPrepare(/* playWhenReady= */ false);
|
||||||
player.setPlayWhenReady(false);
|
|
||||||
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
|
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1076,8 +1091,7 @@ public final class MediaSessionConnector {
|
|||||||
@Override
|
@Override
|
||||||
public void onPrepareFromSearch(String query, Bundle extras) {
|
public void onPrepareFromSearch(String query, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
|
||||||
player.stop();
|
stopPlayerForPrepare(/* playWhenReady= */ false);
|
||||||
player.setPlayWhenReady(false);
|
|
||||||
playbackPreparer.onPrepareFromSearch(query, extras);
|
playbackPreparer.onPrepareFromSearch(query, extras);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1085,8 +1099,7 @@ public final class MediaSessionConnector {
|
|||||||
@Override
|
@Override
|
||||||
public void onPrepareFromUri(Uri uri, Bundle extras) {
|
public void onPrepareFromUri(Uri uri, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
|
||||||
player.stop();
|
stopPlayerForPrepare(/* playWhenReady= */ false);
|
||||||
player.setPlayWhenReady(false);
|
|
||||||
playbackPreparer.onPrepareFromUri(uri, extras);
|
playbackPreparer.onPrepareFromUri(uri, extras);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1094,8 +1107,7 @@ public final class MediaSessionConnector {
|
|||||||
@Override
|
@Override
|
||||||
public void onPlayFromMediaId(String mediaId, Bundle extras) {
|
public void onPlayFromMediaId(String mediaId, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
|
||||||
player.stop();
|
stopPlayerForPrepare(/* playWhenReady= */ true);
|
||||||
player.setPlayWhenReady(true);
|
|
||||||
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
|
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1103,8 +1115,7 @@ public final class MediaSessionConnector {
|
|||||||
@Override
|
@Override
|
||||||
public void onPlayFromSearch(String query, Bundle extras) {
|
public void onPlayFromSearch(String query, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
|
||||||
player.stop();
|
stopPlayerForPrepare(/* playWhenReady= */ true);
|
||||||
player.setPlayWhenReady(true);
|
|
||||||
playbackPreparer.onPrepareFromSearch(query, extras);
|
playbackPreparer.onPrepareFromSearch(query, extras);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1112,8 +1123,7 @@ public final class MediaSessionConnector {
|
|||||||
@Override
|
@Override
|
||||||
public void onPlayFromUri(Uri uri, Bundle extras) {
|
public void onPlayFromUri(Uri uri, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
|
||||||
player.stop();
|
stopPlayerForPrepare(/* playWhenReady= */ true);
|
||||||
player.setPlayWhenReady(true);
|
|
||||||
playbackPreparer.onPrepareFromUri(uri, extras);
|
playbackPreparer.onPrepareFromUri(uri, extras);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,7 @@ import com.google.android.exoplayer2.ControlDispatcher;
|
|||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.util.RepeatModeUtil;
|
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 {
|
public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
|
||||||
|
|
||||||
/** The default repeat toggle modes. */
|
/** The default repeat toggle modes. */
|
||||||
|
@ -65,13 +65,6 @@ public final class TimelineQueueEditor
|
|||||||
* {@link MediaSessionConnector}.
|
* {@link MediaSessionConnector}.
|
||||||
*/
|
*/
|
||||||
public interface QueueDataAdapter {
|
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}.
|
* Adds a {@link MediaDescriptionCompat} at the given {@code position}.
|
||||||
*
|
*
|
||||||
|
@ -41,7 +41,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
|
|||||||
|
|
||||||
private final MediaSessionCompat mediaSession;
|
private final MediaSessionCompat mediaSession;
|
||||||
private final Timeline.Window window;
|
private final Timeline.Window window;
|
||||||
protected final int maxQueueSize;
|
private final int maxQueueSize;
|
||||||
|
|
||||||
private long activeQueueItemId;
|
private long activeQueueItemId;
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import android.net.Uri;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
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.BaseDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceException;
|
import com.google.android.exoplayer2.upstream.DataSourceException;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
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 {
|
private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
|
||||||
long position = dataSpec.position;
|
long position = dataSpec.position;
|
||||||
long length = dataSpec.length;
|
long length = dataSpec.length;
|
||||||
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
|
|
||||||
|
|
||||||
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
|
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
@ -293,10 +293,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
if (userAgent != null) {
|
if (userAgent != null) {
|
||||||
builder.addHeader("User-Agent", userAgent);
|
builder.addHeader("User-Agent", userAgent);
|
||||||
}
|
}
|
||||||
|
if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
|
||||||
if (!allowGzip) {
|
|
||||||
builder.addHeader("Accept-Encoding", "identity");
|
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;
|
RequestBody requestBody = null;
|
||||||
if (dataSpec.httpBody != null) {
|
if (dataSpec.httpBody != null) {
|
||||||
requestBody = RequestBody.create(null, dataSpec.httpBody);
|
requestBody = RequestBody.create(null, dataSpec.httpBody);
|
||||||
|
@ -39,7 +39,7 @@ either instantiated and injected from application code, or obtained from
|
|||||||
instances of `DataSource.Factory` that are instantiated and injected from
|
instances of `DataSource.Factory` that are instantiated and injected from
|
||||||
application code.
|
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
|
available. Hence if your application is using `DefaultDataSource` or
|
||||||
`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as
|
`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as
|
||||||
adding a dependency to the RTMP extension as described above. No changes to your
|
adding a dependency to the RTMP extension as described above. No changes to your
|
||||||
|
@ -127,8 +127,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
private VpxDecoder decoder;
|
private VpxDecoder decoder;
|
||||||
private VpxInputBuffer inputBuffer;
|
private VpxInputBuffer inputBuffer;
|
||||||
private VpxOutputBuffer outputBuffer;
|
private VpxOutputBuffer outputBuffer;
|
||||||
private DrmSession<ExoMediaCrypto> drmSession;
|
@Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
|
||||||
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
@Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;
|
||||||
|
|
||||||
private @ReinitializationState int decoderReinitializationState;
|
private @ReinitializationState int decoderReinitializationState;
|
||||||
private boolean decoderReceivedBuffers;
|
private boolean decoderReceivedBuffers;
|
||||||
@ -364,24 +364,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
clearReportedVideoSize();
|
clearReportedVideoSize();
|
||||||
clearRenderedFirstFrame();
|
clearRenderedFirstFrame();
|
||||||
try {
|
try {
|
||||||
|
setSourceDrmSession(null);
|
||||||
releaseDecoder();
|
releaseDecoder();
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
eventDispatcher.disabled(decoderCounters);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,18 +419,35 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
/** Releases the decoder. */
|
/** Releases the decoder. */
|
||||||
@CallSuper
|
@CallSuper
|
||||||
protected void releaseDecoder() {
|
protected void releaseDecoder() {
|
||||||
if (decoder == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
decoder.release();
|
|
||||||
decoder = null;
|
|
||||||
decoderCounters.decoderReleaseCount++;
|
|
||||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||||
decoderReceivedBuffers = false;
|
decoderReceivedBuffers = false;
|
||||||
buffersInCodecCount = 0;
|
buffersInCodecCount = 0;
|
||||||
|
if (decoder != null) {
|
||||||
|
decoder.release();
|
||||||
|
decoder = null;
|
||||||
|
decoderCounters.decoderReleaseCount++;
|
||||||
|
}
|
||||||
|
setDecoderDrmSession(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
|
||||||
|
DrmSession<ExoMediaCrypto> previous = sourceDrmSession;
|
||||||
|
sourceDrmSession = session;
|
||||||
|
releaseDrmSessionIfUnused(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
|
||||||
|
DrmSession<ExoMediaCrypto> previous = decoderDrmSession;
|
||||||
|
decoderDrmSession = session;
|
||||||
|
releaseDrmSessionIfUnused(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseDrmSessionIfUnused(@Nullable DrmSession<ExoMediaCrypto> session) {
|
||||||
|
if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
|
||||||
|
drmSessionManager.releaseSession(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -467,16 +470,20 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
throw ExoPlaybackException.createForRenderer(
|
throw ExoPlaybackException.createForRenderer(
|
||||||
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
||||||
}
|
}
|
||||||
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
|
DrmSession<ExoMediaCrypto> session =
|
||||||
if (pendingDrmSession == drmSession) {
|
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
|
||||||
drmSessionManager.releaseSession(pendingDrmSession);
|
if (session == decoderDrmSession || session == sourceDrmSession) {
|
||||||
|
// We already had this session. The manager must be reference counting, so release it once
|
||||||
|
// to get the count attributed to this renderer back down to 1.
|
||||||
|
drmSessionManager.releaseSession(session);
|
||||||
}
|
}
|
||||||
|
setSourceDrmSession(session);
|
||||||
} else {
|
} else {
|
||||||
pendingDrmSession = null;
|
setSourceDrmSession(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingDrmSession != drmSession) {
|
if (sourceDrmSession != decoderDrmSession) {
|
||||||
if (decoderReceivedBuffers) {
|
if (decoderReceivedBuffers) {
|
||||||
// Signal end of stream and wait for any final output buffers before re-initialization.
|
// Signal end of stream and wait for any final output buffers before re-initialization.
|
||||||
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
|
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
|
||||||
@ -704,12 +711,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
drmSession = pendingDrmSession;
|
setDecoderDrmSession(sourceDrmSession);
|
||||||
|
|
||||||
ExoMediaCrypto mediaCrypto = null;
|
ExoMediaCrypto mediaCrypto = null;
|
||||||
if (drmSession != null) {
|
if (decoderDrmSession != null) {
|
||||||
mediaCrypto = drmSession.getMediaCrypto();
|
mediaCrypto = decoderDrmSession.getMediaCrypto();
|
||||||
if (mediaCrypto == null) {
|
if (mediaCrypto == null) {
|
||||||
DrmSessionException drmError = drmSession.getError();
|
DrmSessionException drmError = decoderDrmSession.getError();
|
||||||
if (drmError != null) {
|
if (drmError != null) {
|
||||||
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
|
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
|
||||||
// input format causes the session to be replaced before it's used.
|
// input format causes the session to be replaced before it's used.
|
||||||
@ -922,12 +930,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||||
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
@DrmSession.State int drmSessionState = decoderDrmSession.getState();
|
||||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
|
||||||
}
|
}
|
||||||
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
||||||
}
|
}
|
||||||
|
@ -460,8 +460,8 @@ public final class C {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
|
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
|
||||||
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_ENCRYPTED} and
|
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE},
|
||||||
* {@link #BUFFER_FLAG_DECODE_ONLY}.
|
* {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}.
|
||||||
*/
|
*/
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@ -470,6 +470,7 @@ public final class C {
|
|||||||
value = {
|
value = {
|
||||||
BUFFER_FLAG_KEY_FRAME,
|
BUFFER_FLAG_KEY_FRAME,
|
||||||
BUFFER_FLAG_END_OF_STREAM,
|
BUFFER_FLAG_END_OF_STREAM,
|
||||||
|
BUFFER_FLAG_LAST_SAMPLE,
|
||||||
BUFFER_FLAG_ENCRYPTED,
|
BUFFER_FLAG_ENCRYPTED,
|
||||||
BUFFER_FLAG_DECODE_ONLY
|
BUFFER_FLAG_DECODE_ONLY
|
||||||
})
|
})
|
||||||
@ -482,6 +483,8 @@ public final class C {
|
|||||||
* Flag for empty buffers that signal that the end of the stream was reached.
|
* Flag for empty buffers that signal that the end of the stream was reached.
|
||||||
*/
|
*/
|
||||||
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
||||||
|
/** Indicates that a buffer is known to contain the last media sample of the stream. */
|
||||||
|
public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000
|
||||||
/** Indicates that a buffer is (at least partially) encrypted. */
|
/** Indicates that a buffer is (at least partially) encrypted. */
|
||||||
public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
|
public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
|
||||||
/** Indicates that a buffer should be decoded but not rendered. */
|
/** Indicates that a buffer should be decoded but not rendered. */
|
||||||
@ -896,6 +899,26 @@ public final class C {
|
|||||||
*/
|
*/
|
||||||
public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
|
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.
|
* Priority for media playback.
|
||||||
*
|
*
|
||||||
|
@ -139,26 +139,34 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||||||
repeatMode,
|
repeatMode,
|
||||||
shuffleModeEnabled,
|
shuffleModeEnabled,
|
||||||
eventHandler,
|
eventHandler,
|
||||||
this,
|
|
||||||
clock);
|
clock);
|
||||||
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
|
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public AudioComponent getAudioComponent() {
|
public AudioComponent getAudioComponent() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public VideoComponent getVideoComponent() {
|
public VideoComponent getVideoComponent() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public TextComponent getTextComponent() {
|
public TextComponent getTextComponent() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public MetadataComponent getMetadataComponent() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Looper getPlaybackLooper() {
|
public Looper getPlaybackLooper() {
|
||||||
return internalPlayer.getPlaybackLooper();
|
return internalPlayer.getPlaybackLooper();
|
||||||
|
@ -95,7 +95,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
private final HandlerWrapper handler;
|
private final HandlerWrapper handler;
|
||||||
private final HandlerThread internalPlaybackThread;
|
private final HandlerThread internalPlaybackThread;
|
||||||
private final Handler eventHandler;
|
private final Handler eventHandler;
|
||||||
private final ExoPlayer player;
|
|
||||||
private final Timeline.Window window;
|
private final Timeline.Window window;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
private final long backBufferDurationUs;
|
private final long backBufferDurationUs;
|
||||||
@ -134,7 +133,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
@Player.RepeatMode int repeatMode,
|
@Player.RepeatMode int repeatMode,
|
||||||
boolean shuffleModeEnabled,
|
boolean shuffleModeEnabled,
|
||||||
Handler eventHandler,
|
Handler eventHandler,
|
||||||
ExoPlayer player,
|
|
||||||
Clock clock) {
|
Clock clock) {
|
||||||
this.renderers = renderers;
|
this.renderers = renderers;
|
||||||
this.trackSelector = trackSelector;
|
this.trackSelector = trackSelector;
|
||||||
@ -145,7 +143,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
this.repeatMode = repeatMode;
|
this.repeatMode = repeatMode;
|
||||||
this.shuffleModeEnabled = shuffleModeEnabled;
|
this.shuffleModeEnabled = shuffleModeEnabled;
|
||||||
this.eventHandler = eventHandler;
|
this.eventHandler = eventHandler;
|
||||||
this.player = player;
|
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
this.queue = new MediaPeriodQueue();
|
this.queue = new MediaPeriodQueue();
|
||||||
|
|
||||||
@ -441,11 +438,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
loadControl.onPrepared();
|
loadControl.onPrepared();
|
||||||
this.mediaSource = mediaSource;
|
this.mediaSource = mediaSource;
|
||||||
setState(Player.STATE_BUFFERING);
|
setState(Player.STATE_BUFFERING);
|
||||||
mediaSource.prepareSource(
|
mediaSource.prepareSource(/* listener= */ this, bandwidthMeter.getTransferListener());
|
||||||
player,
|
|
||||||
/* isTopLevelSource= */ true,
|
|
||||||
/* listener= */ this,
|
|
||||||
bandwidthMeter.getTransferListener());
|
|
||||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
|
|||||||
|
|
||||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||||
public static final String VERSION = "2.9.2";
|
public static final String VERSION = "2.9.4";
|
||||||
|
|
||||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.2";
|
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.4";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the library expressed as an integer, for example 1002003.
|
* The version of the library expressed as an integer, for example 1002003.
|
||||||
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
|
|||||||
* integer version 123045006 (123-045-006).
|
* integer version 123045006 (123-045-006).
|
||||||
*/
|
*/
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final int VERSION_INT = 2009002;
|
public static final int VERSION_INT = 2009004;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||||
|
@ -1181,6 +1181,37 @@ public final class Format implements Parcelable {
|
|||||||
metadata);
|
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) {
|
public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {
|
||||||
return new Format(
|
return new Format(
|
||||||
id,
|
id,
|
||||||
@ -1274,6 +1305,37 @@ public final class Format implements Parcelable {
|
|||||||
metadata);
|
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}
|
* Returns the number of pixels if this is a video format whose {@link #width} and {@link #height}
|
||||||
* are known, or {@link #NO_VALUE} otherwise
|
* are known, or {@link #NO_VALUE} otherwise
|
||||||
|
@ -89,7 +89,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
this.info = info;
|
this.info = info;
|
||||||
sampleStreams = new SampleStream[rendererCapabilities.length];
|
sampleStreams = new SampleStream[rendererCapabilities.length];
|
||||||
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
|
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
|
||||||
mediaPeriod = createMediaPeriod(info.id, mediaSource, allocator);
|
mediaPeriod = createMediaPeriod(info.id, mediaSource, allocator, info.startPositionUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -399,8 +399,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
|
|
||||||
/** Returns a media period corresponding to the given {@code id}. */
|
/** Returns a media period corresponding to the given {@code id}. */
|
||||||
private static MediaPeriod createMediaPeriod(
|
private static MediaPeriod createMediaPeriod(
|
||||||
MediaPeriodId id, MediaSource mediaSource, Allocator allocator) {
|
MediaPeriodId id, MediaSource mediaSource, Allocator allocator, long startPositionUs) {
|
||||||
MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator);
|
MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs);
|
||||||
if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) {
|
if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) {
|
||||||
mediaPeriod =
|
mediaPeriod =
|
||||||
new ClippingMediaPeriod(
|
new ClippingMediaPeriod(
|
||||||
|
@ -26,6 +26,7 @@ import com.google.android.exoplayer2.C.VideoScalingMode;
|
|||||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||||
import com.google.android.exoplayer2.audio.AudioListener;
|
import com.google.android.exoplayer2.audio.AudioListener;
|
||||||
import com.google.android.exoplayer2.audio.AuxEffectInfo;
|
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.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.text.TextOutput;
|
import com.google.android.exoplayer2.text.TextOutput;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
@ -299,6 +300,24 @@ public interface Player {
|
|||||||
void removeTextOutput(TextOutput listener);
|
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
|
* Listener of changes in player state. All methods have no-op default implementations to allow
|
||||||
* selective overrides.
|
* selective overrides.
|
||||||
@ -533,6 +552,12 @@ public interface Player {
|
|||||||
@Nullable
|
@Nullable
|
||||||
TextComponent getTextComponent();
|
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
|
* Returns the {@link Looper} associated with the application thread that's used to access the
|
||||||
* player and on which player events are received.
|
* player and on which player events are received.
|
||||||
|
@ -65,7 +65,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
@TargetApi(16)
|
||||||
public class SimpleExoPlayer extends BasePlayer
|
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 Use {@link com.google.android.exoplayer2.video.VideoListener}. */
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@ -90,25 +94,25 @@ public class SimpleExoPlayer extends BasePlayer
|
|||||||
|
|
||||||
private final AudioFocusManager audioFocusManager;
|
private final AudioFocusManager audioFocusManager;
|
||||||
|
|
||||||
private Format videoFormat;
|
@Nullable private Format videoFormat;
|
||||||
private Format audioFormat;
|
@Nullable private Format audioFormat;
|
||||||
|
|
||||||
private Surface surface;
|
@Nullable private Surface surface;
|
||||||
private boolean ownsSurface;
|
private boolean ownsSurface;
|
||||||
private @C.VideoScalingMode int videoScalingMode;
|
private @C.VideoScalingMode int videoScalingMode;
|
||||||
private SurfaceHolder surfaceHolder;
|
@Nullable private SurfaceHolder surfaceHolder;
|
||||||
private TextureView textureView;
|
@Nullable private TextureView textureView;
|
||||||
private int surfaceWidth;
|
private int surfaceWidth;
|
||||||
private int surfaceHeight;
|
private int surfaceHeight;
|
||||||
private DecoderCounters videoDecoderCounters;
|
@Nullable private DecoderCounters videoDecoderCounters;
|
||||||
private DecoderCounters audioDecoderCounters;
|
@Nullable private DecoderCounters audioDecoderCounters;
|
||||||
private int audioSessionId;
|
private int audioSessionId;
|
||||||
private AudioAttributes audioAttributes;
|
private AudioAttributes audioAttributes;
|
||||||
private float audioVolume;
|
private float audioVolume;
|
||||||
private MediaSource mediaSource;
|
@Nullable private MediaSource mediaSource;
|
||||||
private List<Cue> currentCues;
|
private List<Cue> currentCues;
|
||||||
private VideoFrameMetadataListener videoFrameMetadataListener;
|
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
|
||||||
private CameraMotionListener cameraMotionListener;
|
@Nullable private CameraMotionListener cameraMotionListener;
|
||||||
private boolean hasNotifiedFullWrongThreadWarning;
|
private boolean hasNotifiedFullWrongThreadWarning;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -243,20 +247,29 @@ public class SimpleExoPlayer extends BasePlayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public AudioComponent getAudioComponent() {
|
public AudioComponent getAudioComponent() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public VideoComponent getVideoComponent() {
|
public VideoComponent getVideoComponent() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public TextComponent getTextComponent() {
|
public TextComponent getTextComponent() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public MetadataComponent getMetadataComponent() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the video scaling mode.
|
* Sets the video scaling mode.
|
||||||
*
|
*
|
||||||
@ -545,30 +558,26 @@ public class SimpleExoPlayer extends BasePlayer
|
|||||||
setPlaybackParameters(playbackParameters);
|
setPlaybackParameters(playbackParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the video format currently being played, or null if no video is being played. */
|
||||||
* Returns the video format currently being played, or null if no video is being played.
|
@Nullable
|
||||||
*/
|
|
||||||
public Format getVideoFormat() {
|
public Format getVideoFormat() {
|
||||||
return videoFormat;
|
return videoFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the audio format currently being played, or null if no audio is being played. */
|
||||||
* Returns the audio format currently being played, or null if no audio is being played.
|
@Nullable
|
||||||
*/
|
|
||||||
public Format getAudioFormat() {
|
public Format getAudioFormat() {
|
||||||
return audioFormat;
|
return audioFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns {@link DecoderCounters} for video, or null if no video is being played. */
|
||||||
* Returns {@link DecoderCounters} for video, or null if no video is being played.
|
@Nullable
|
||||||
*/
|
|
||||||
public DecoderCounters getVideoDecoderCounters() {
|
public DecoderCounters getVideoDecoderCounters() {
|
||||||
return videoDecoderCounters;
|
return videoDecoderCounters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns {@link DecoderCounters} for audio, or null if no audio is being played. */
|
||||||
* Returns {@link DecoderCounters} for audio, or null if no audio is being played.
|
@Nullable
|
||||||
*/
|
|
||||||
public DecoderCounters getAudioDecoderCounters() {
|
public DecoderCounters getAudioDecoderCounters() {
|
||||||
return audioDecoderCounters;
|
return audioDecoderCounters;
|
||||||
}
|
}
|
||||||
@ -713,20 +722,12 @@ public class SimpleExoPlayer extends BasePlayer
|
|||||||
removeTextOutput(output);
|
removeTextOutput(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Adds a {@link MetadataOutput} to receive metadata.
|
|
||||||
*
|
|
||||||
* @param listener The output to register.
|
|
||||||
*/
|
|
||||||
public void addMetadataOutput(MetadataOutput listener) {
|
public void addMetadataOutput(MetadataOutput listener) {
|
||||||
metadataOutputs.add(listener);
|
metadataOutputs.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Removes a {@link MetadataOutput}.
|
|
||||||
*
|
|
||||||
* @param listener The output to remove.
|
|
||||||
*/
|
|
||||||
public void removeMetadataOutput(MetadataOutput listener) {
|
public void removeMetadataOutput(MetadataOutput listener) {
|
||||||
metadataOutputs.remove(listener);
|
metadataOutputs.remove(listener);
|
||||||
}
|
}
|
||||||
@ -1048,7 +1049,8 @@ public class SimpleExoPlayer extends BasePlayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object getCurrentManifest() {
|
@Nullable
|
||||||
|
public Object getCurrentManifest() {
|
||||||
verifyApplicationThread();
|
verifyApplicationThread();
|
||||||
return player.getCurrentManifest();
|
return player.getCurrentManifest();
|
||||||
}
|
}
|
||||||
|
@ -488,7 +488,10 @@ public class AnalyticsCollector
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void onPlayerError(ExoPlaybackException error) {
|
public final void onPlayerError(ExoPlaybackException error) {
|
||||||
EventTime eventTime = generatePlayingMediaPeriodEventTime();
|
EventTime eventTime =
|
||||||
|
error.type == ExoPlaybackException.TYPE_SOURCE
|
||||||
|
? generateLoadingMediaPeriodEventTime()
|
||||||
|
: generatePlayingMediaPeriodEventTime();
|
||||||
for (AnalyticsListener listener : listeners) {
|
for (AnalyticsListener listener : listeners) {
|
||||||
listener.onPlayerError(eventTime, error);
|
listener.onPlayerError(eventTime, error);
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,7 @@ public interface AudioRendererEventListener {
|
|||||||
* Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
|
* Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
|
||||||
*/
|
*/
|
||||||
public void disabled(final DecoderCounters counters) {
|
public void disabled(final DecoderCounters counters) {
|
||||||
|
counters.ensureUpdated();
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
handler.post(
|
handler.post(
|
||||||
() -> {
|
() -> {
|
||||||
|
@ -548,7 +548,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
try {
|
try {
|
||||||
super.onDisabled();
|
super.onDisabled();
|
||||||
} finally {
|
} finally {
|
||||||
decoderCounters.ensureUpdated();
|
|
||||||
eventDispatcher.disabled(decoderCounters);
|
eventDispatcher.disabled(decoderCounters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,8 +106,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
? extends AudioDecoderException> decoder;
|
? extends AudioDecoderException> decoder;
|
||||||
private DecoderInputBuffer inputBuffer;
|
private DecoderInputBuffer inputBuffer;
|
||||||
private SimpleOutputBuffer outputBuffer;
|
private SimpleOutputBuffer outputBuffer;
|
||||||
private DrmSession<ExoMediaCrypto> drmSession;
|
@Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
|
||||||
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
@Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;
|
||||||
|
|
||||||
@ReinitializationState private int decoderReinitializationState;
|
@ReinitializationState private int decoderReinitializationState;
|
||||||
private boolean decoderReceivedBuffers;
|
private boolean decoderReceivedBuffers;
|
||||||
@ -366,7 +366,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
if (outputBuffer == null) {
|
if (outputBuffer == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
|
if (outputBuffer.skippedOutputBufferCount > 0) {
|
||||||
|
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
|
||||||
|
audioSink.handleDiscontinuity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outputBuffer.isEndOfStream()) {
|
if (outputBuffer.isEndOfStream()) {
|
||||||
@ -459,12 +462,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||||
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
@DrmSession.State int drmSessionState = decoderDrmSession.getState();
|
||||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
|
||||||
}
|
}
|
||||||
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
||||||
}
|
}
|
||||||
@ -565,25 +568,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
audioTrackNeedsConfigure = true;
|
audioTrackNeedsConfigure = true;
|
||||||
waitingForKeys = false;
|
waitingForKeys = false;
|
||||||
try {
|
try {
|
||||||
|
setSourceDrmSession(null);
|
||||||
releaseDecoder();
|
releaseDecoder();
|
||||||
audioSink.reset();
|
audioSink.reset();
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
eventDispatcher.disabled(decoderCounters);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,12 +601,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
drmSession = pendingDrmSession;
|
setDecoderDrmSession(sourceDrmSession);
|
||||||
|
|
||||||
ExoMediaCrypto mediaCrypto = null;
|
ExoMediaCrypto mediaCrypto = null;
|
||||||
if (drmSession != null) {
|
if (decoderDrmSession != null) {
|
||||||
mediaCrypto = drmSession.getMediaCrypto();
|
mediaCrypto = decoderDrmSession.getMediaCrypto();
|
||||||
if (mediaCrypto == null) {
|
if (mediaCrypto == null) {
|
||||||
DrmSessionException drmError = drmSession.getError();
|
DrmSessionException drmError = decoderDrmSession.getError();
|
||||||
if (drmError != null) {
|
if (drmError != null) {
|
||||||
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
|
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
|
||||||
// input format causes the session to be replaced before it's used.
|
// input format causes the session to be replaced before it's used.
|
||||||
@ -643,17 +633,34 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void releaseDecoder() {
|
private void releaseDecoder() {
|
||||||
if (decoder == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
decoder.release();
|
|
||||||
decoder = null;
|
|
||||||
decoderCounters.decoderReleaseCount++;
|
|
||||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||||
decoderReceivedBuffers = false;
|
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 {
|
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
|
||||||
@ -668,13 +675,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||||||
throw ExoPlaybackException.createForRenderer(
|
throw ExoPlaybackException.createForRenderer(
|
||||||
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
||||||
}
|
}
|
||||||
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(),
|
DrmSession<ExoMediaCrypto> session =
|
||||||
inputFormat.drmInitData);
|
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
|
||||||
if (pendingDrmSession == drmSession) {
|
if (session == decoderDrmSession || session == sourceDrmSession) {
|
||||||
drmSessionManager.releaseSession(pendingDrmSession);
|
// We already had this session. The manager must be reference counting, so release it once
|
||||||
|
// to get the count attributed to this renderer back down to 1.
|
||||||
|
drmSessionManager.releaseSession(session);
|
||||||
}
|
}
|
||||||
|
setSourceDrmSession(session);
|
||||||
} else {
|
} else {
|
||||||
pendingDrmSession = null;
|
setSourceDrmSession(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.database;
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides {@link SQLiteDatabase} instances to ExoPlayer components, which may read and write
|
||||||
|
* tables prefixed with {@link #TABLE_PREFIX}.
|
||||||
|
*/
|
||||||
|
public interface DatabaseProvider {
|
||||||
|
|
||||||
|
/** Prefix for tables that can be read and written by ExoPlayer components. */
|
||||||
|
String TABLE_PREFIX = "ExoPlayer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and/or opens a database that will be used for reading and writing.
|
||||||
|
*
|
||||||
|
* <p>Once opened successfully, the database is cached, so you can call this method every time you
|
||||||
|
* need to write to the database. Errors such as bad permissions or a full disk may cause this
|
||||||
|
* method to fail, but future attempts may succeed if the problem is fixed.
|
||||||
|
*
|
||||||
|
* @throws SQLiteException If the database cannot be opened for writing.
|
||||||
|
* @return A read/write database object.
|
||||||
|
*/
|
||||||
|
SQLiteDatabase getWritableDatabase();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and/or opens a database. This will be the same object returned by {@link
|
||||||
|
* #getWritableDatabase()} unless some problem, such as a full disk, requires the database to be
|
||||||
|
* opened read-only. In that case, a read-only database object will be returned. If the problem is
|
||||||
|
* fixed, a future call to {@link #getWritableDatabase()} may succeed, in which case the read-only
|
||||||
|
* database object will be closed and the read/write object will be returned in the future.
|
||||||
|
*
|
||||||
|
* <p>Once opened successfully, the database is cached, so you can call this method every time you
|
||||||
|
* need to read from the database.
|
||||||
|
*
|
||||||
|
* @throws SQLiteException If the database cannot be opened.
|
||||||
|
* @return A database object valid until {@link #getWritableDatabase()} is called.
|
||||||
|
*/
|
||||||
|
SQLiteDatabase getReadableDatabase();
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.database;
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
|
||||||
|
/** A {@link DatabaseProvider} that provides instances obtained from a {@link SQLiteOpenHelper}. */
|
||||||
|
public final class DefaultDatabaseProvider implements DatabaseProvider {
|
||||||
|
|
||||||
|
private final SQLiteOpenHelper sqliteOpenHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sqliteOpenHelper An {@link SQLiteOpenHelper} from which to obtain database instances.
|
||||||
|
*/
|
||||||
|
public DefaultDatabaseProvider(SQLiteOpenHelper sqliteOpenHelper) {
|
||||||
|
this.sqliteOpenHelper = sqliteOpenHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLiteDatabase getWritableDatabase() {
|
||||||
|
return sqliteOpenHelper.getWritableDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLiteDatabase getReadableDatabase() {
|
||||||
|
return sqliteOpenHelper.getReadableDatabase();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -15,14 +15,5 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.drm;
|
package com.google.android.exoplayer2.drm;
|
||||||
|
|
||||||
/**
|
/** An opaque {@link android.media.MediaCrypto} equivalent. */
|
||||||
* An opaque {@link android.media.MediaCrypto} equivalent.
|
public interface ExoMediaCrypto {}
|
||||||
*/
|
|
||||||
public interface ExoMediaCrypto {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see android.media.MediaCrypto#requiresSecureDecoderComponent(String)
|
|
||||||
*/
|
|
||||||
boolean requiresSecureDecoderComponent(String mimeType);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -265,11 +265,9 @@ public interface ExoMediaDrm<T extends ExoMediaCrypto> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see android.media.MediaCrypto#MediaCrypto(UUID, byte[])
|
* @see android.media.MediaCrypto#MediaCrypto(UUID, byte[])
|
||||||
*
|
* @param sessionId The DRM session ID.
|
||||||
* @param initData Opaque initialization data specific to the crypto scheme.
|
|
||||||
* @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.
|
* @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.
|
||||||
* @throws MediaCryptoException If the instance can't be created.
|
* @throws MediaCryptoException If the instance can't be created.
|
||||||
*/
|
*/
|
||||||
T createMediaCrypto(byte[] initData) throws MediaCryptoException;
|
T createMediaCrypto(byte[] sessionId) throws MediaCryptoException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,48 +17,35 @@ package com.google.android.exoplayer2.drm;
|
|||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.media.MediaCrypto;
|
import android.media.MediaCrypto;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link ExoMediaCrypto} implementation that wraps the framework {@link MediaCrypto}.
|
* An {@link ExoMediaCrypto} implementation that contains the necessary information to build or
|
||||||
|
* update a framework {@link MediaCrypto}.
|
||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
@TargetApi(16)
|
||||||
public final class FrameworkMediaCrypto implements ExoMediaCrypto {
|
public final class FrameworkMediaCrypto implements ExoMediaCrypto {
|
||||||
|
|
||||||
private final MediaCrypto mediaCrypto;
|
/** The DRM scheme UUID. */
|
||||||
private final boolean forceAllowInsecureDecoderComponents;
|
public final UUID uuid;
|
||||||
|
/** The DRM session id. */
|
||||||
|
public final byte[] sessionId;
|
||||||
|
/**
|
||||||
|
* Whether to allow use of insecure decoder components even if the underlying platform says
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public final boolean forceAllowInsecureDecoderComponents;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mediaCrypto The {@link MediaCrypto} to wrap.
|
* @param uuid The DRM scheme UUID.
|
||||||
|
* @param sessionId The DRM session id.
|
||||||
|
* @param forceAllowInsecureDecoderComponents Whether to allow use of insecure decoder components
|
||||||
|
* even if the underlying platform says otherwise.
|
||||||
*/
|
*/
|
||||||
public FrameworkMediaCrypto(MediaCrypto mediaCrypto) {
|
public FrameworkMediaCrypto(
|
||||||
this(mediaCrypto, false);
|
UUID uuid, byte[] sessionId, boolean forceAllowInsecureDecoderComponents) {
|
||||||
}
|
this.uuid = uuid;
|
||||||
|
this.sessionId = sessionId;
|
||||||
/**
|
|
||||||
* @param mediaCrypto The {@link MediaCrypto} to wrap.
|
|
||||||
* @param forceAllowInsecureDecoderComponents Whether to force
|
|
||||||
* {@link #requiresSecureDecoderComponent(String)} to return {@code false}, rather than
|
|
||||||
* {@link MediaCrypto#requiresSecureDecoderComponent(String)} of the wrapped
|
|
||||||
* {@link MediaCrypto}.
|
|
||||||
*/
|
|
||||||
public FrameworkMediaCrypto(MediaCrypto mediaCrypto,
|
|
||||||
boolean forceAllowInsecureDecoderComponents) {
|
|
||||||
this.mediaCrypto = Assertions.checkNotNull(mediaCrypto);
|
|
||||||
this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;
|
this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the wrapped {@link MediaCrypto}.
|
|
||||||
*/
|
|
||||||
public MediaCrypto getWrappedMediaCrypto() {
|
|
||||||
return mediaCrypto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean requiresSecureDecoderComponent(String mimeType) {
|
|
||||||
return !forceAllowInsecureDecoderComponents
|
|
||||||
&& mediaCrypto.requiresSecureDecoderComponent(mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ package com.google.android.exoplayer2.drm;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.media.DeniedByServerException;
|
import android.media.DeniedByServerException;
|
||||||
import android.media.MediaCrypto;
|
|
||||||
import android.media.MediaCryptoException;
|
import android.media.MediaCryptoException;
|
||||||
import android.media.MediaDrm;
|
import android.media.MediaDrm;
|
||||||
import android.media.MediaDrmException;
|
import android.media.MediaDrmException;
|
||||||
@ -210,7 +209,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
|
|||||||
boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
|
boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
|
||||||
&& C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
|
&& C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
|
||||||
return new FrameworkMediaCrypto(
|
return new FrameworkMediaCrypto(
|
||||||
new MediaCrypto(adjustUuid(uuid), initData), forceAllowInsecureDecoderComponents);
|
adjustUuid(uuid), initData, forceAllowInsecureDecoderComponents);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) {
|
private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) {
|
||||||
|
@ -34,16 +34,26 @@ public final class MpegAudioHeader {
|
|||||||
private static final String[] MIME_TYPE_BY_LAYER =
|
private static final String[] MIME_TYPE_BY_LAYER =
|
||||||
new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG};
|
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[] SAMPLING_RATE_V1 = {44100, 48000, 32000};
|
||||||
private static final int[] BITRATE_V1_L1 =
|
private static final int[] BITRATE_V1_L1 = {
|
||||||
{32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448};
|
32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000,
|
||||||
private static final int[] BITRATE_V2_L1 =
|
416000, 448000
|
||||||
{32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256};
|
};
|
||||||
private static final int[] BITRATE_V1_L2 =
|
private static final int[] BITRATE_V2_L1 = {
|
||||||
{32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384};
|
32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000,
|
||||||
private static final int[] BITRATE_V1_L3 =
|
224000, 256000
|
||||||
{32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320};
|
};
|
||||||
private static final int[] BITRATE_V2 =
|
private static final int[] BITRATE_V1_L2 = {
|
||||||
{8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160};
|
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
|
* 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) {
|
if (layer == 3) {
|
||||||
// Layer I (layer == 3)
|
// Layer I (layer == 3)
|
||||||
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
|
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 {
|
} else {
|
||||||
// Layer II (layer == 2) or III (layer == 1)
|
// Layer II (layer == 2) or III (layer == 1)
|
||||||
if (version == 3) {
|
if (version == 3) {
|
||||||
@ -102,10 +112,10 @@ public final class MpegAudioHeader {
|
|||||||
|
|
||||||
if (version == 3) {
|
if (version == 3) {
|
||||||
// Version 1
|
// Version 1
|
||||||
return 144000 * bitrate / samplingRate + padding;
|
return 144 * bitrate / samplingRate + padding;
|
||||||
} else {
|
} else {
|
||||||
// Version 2 or 2.5
|
// 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) {
|
if (layer == 3) {
|
||||||
// Layer I (layer == 3)
|
// Layer I (layer == 3)
|
||||||
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
|
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;
|
samplesPerFrame = 384;
|
||||||
} else {
|
} else {
|
||||||
// Layer II (layer == 2) or III (layer == 1)
|
// Layer II (layer == 2) or III (layer == 1)
|
||||||
@ -167,19 +177,22 @@ public final class MpegAudioHeader {
|
|||||||
// Version 1
|
// Version 1
|
||||||
bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
|
bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
|
||||||
samplesPerFrame = 1152;
|
samplesPerFrame = 1152;
|
||||||
frameSize = 144000 * bitrate / sampleRate + padding;
|
frameSize = 144 * bitrate / sampleRate + padding;
|
||||||
} else {
|
} else {
|
||||||
// Version 2 or 2.5.
|
// Version 2 or 2.5.
|
||||||
bitrate = BITRATE_V2[bitrateIndex - 1];
|
bitrate = BITRATE_V2[bitrateIndex - 1];
|
||||||
samplesPerFrame = layer == 1 ? 576 : 1152;
|
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];
|
String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
|
||||||
int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
|
int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
|
||||||
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000,
|
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame);
|
||||||
samplesPerFrame);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,8 +211,14 @@ public final class MpegAudioHeader {
|
|||||||
/** Number of samples stored in the frame. */
|
/** Number of samples stored in the frame. */
|
||||||
public int samplesPerFrame;
|
public int samplesPerFrame;
|
||||||
|
|
||||||
private void setValues(int version, String mimeType, int frameSize, int sampleRate, int channels,
|
private void setValues(
|
||||||
int bitrate, int samplesPerFrame) {
|
int version,
|
||||||
|
String mimeType,
|
||||||
|
int frameSize,
|
||||||
|
int sampleRate,
|
||||||
|
int channels,
|
||||||
|
int bitrate,
|
||||||
|
int samplesPerFrame) {
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
this.frameSize = frameSize;
|
this.frameSize = frameSize;
|
||||||
|
@ -191,7 +191,11 @@ public final class MatroskaExtractor implements Extractor {
|
|||||||
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
|
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
|
||||||
private static final int ID_LANGUAGE = 0x22B59C;
|
private static final int ID_LANGUAGE = 0x22B59C;
|
||||||
private static final int ID_PROJECTION = 0x7670;
|
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_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_STEREO_MODE = 0x53B8;
|
||||||
private static final int ID_COLOUR = 0x55B0;
|
private static final int ID_COLOUR = 0x55B0;
|
||||||
private static final int ID_COLOUR_RANGE = 0x55B9;
|
private static final int ID_COLOUR_RANGE = 0x55B9;
|
||||||
@ -760,6 +764,24 @@ public final class MatroskaExtractor implements Extractor {
|
|||||||
case ID_MAX_FALL:
|
case ID_MAX_FALL:
|
||||||
currentTrack.maxFrameAverageLuminance = (int) value;
|
currentTrack.maxFrameAverageLuminance = (int) value;
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -803,6 +825,15 @@ public final class MatroskaExtractor implements Extractor {
|
|||||||
case ID_LUMNINANCE_MIN:
|
case ID_LUMNINANCE_MIN:
|
||||||
currentTrack.minMasteringLuminance = (float) value;
|
currentTrack.minMasteringLuminance = (float) value;
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1465,6 +1496,7 @@ public final class MatroskaExtractor implements Extractor {
|
|||||||
case ID_COLOUR_PRIMARIES:
|
case ID_COLOUR_PRIMARIES:
|
||||||
case ID_MAX_CLL:
|
case ID_MAX_CLL:
|
||||||
case ID_MAX_FALL:
|
case ID_MAX_FALL:
|
||||||
|
case ID_PROJECTION_TYPE:
|
||||||
return TYPE_UNSIGNED_INT;
|
return TYPE_UNSIGNED_INT;
|
||||||
case ID_DOC_TYPE:
|
case ID_DOC_TYPE:
|
||||||
case ID_NAME:
|
case ID_NAME:
|
||||||
@ -1491,6 +1523,9 @@ public final class MatroskaExtractor implements Extractor {
|
|||||||
case ID_WHITE_POINT_CHROMATICITY_Y:
|
case ID_WHITE_POINT_CHROMATICITY_Y:
|
||||||
case ID_LUMNINANCE_MAX:
|
case ID_LUMNINANCE_MAX:
|
||||||
case ID_LUMNINANCE_MIN:
|
case ID_LUMNINANCE_MIN:
|
||||||
|
case ID_PROJECTION_POSE_YAW:
|
||||||
|
case ID_PROJECTION_POSE_PITCH:
|
||||||
|
case ID_PROJECTION_POSE_ROLL:
|
||||||
return TYPE_FLOAT;
|
return TYPE_FLOAT;
|
||||||
default:
|
default:
|
||||||
return TYPE_UNKNOWN;
|
return TYPE_UNKNOWN;
|
||||||
@ -1631,6 +1666,10 @@ public final class MatroskaExtractor implements Extractor {
|
|||||||
public int displayWidth = Format.NO_VALUE;
|
public int displayWidth = Format.NO_VALUE;
|
||||||
public int displayHeight = Format.NO_VALUE;
|
public int displayHeight = Format.NO_VALUE;
|
||||||
public int displayUnit = DISPLAY_UNIT_PIXELS;
|
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;
|
public byte[] projectionData = null;
|
||||||
@C.StereoMode
|
@C.StereoMode
|
||||||
public int stereoMode = Format.NO_VALUE;
|
public int stereoMode = Format.NO_VALUE;
|
||||||
@ -1850,6 +1889,21 @@ public final class MatroskaExtractor implements Extractor {
|
|||||||
} else if ("htc_video_rotA-270".equals(name)) {
|
} else if ("htc_video_rotA-270".equals(name)) {
|
||||||
rotationDegrees = 270;
|
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 =
|
||||||
Format.createVideoSampleFormat(
|
Format.createVideoSampleFormat(
|
||||||
Integer.toString(trackId),
|
Integer.toString(trackId),
|
||||||
|
@ -22,7 +22,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@SuppressWarnings("ConstantField")
|
@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"})
|
||||||
/* package */ abstract class Atom {
|
/* 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_sawb = Util.getIntegerCodeForString("sawb");
|
||||||
public static final int TYPE_udta = Util.getIntegerCodeForString("udta");
|
public static final int TYPE_udta = Util.getIntegerCodeForString("udta");
|
||||||
public static final int TYPE_meta = Util.getIntegerCodeForString("meta");
|
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_ilst = Util.getIntegerCodeForString("ilst");
|
||||||
public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
|
public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
|
||||||
public static final int TYPE_name = Util.getIntegerCodeForString("name");
|
public static final int TYPE_name = Util.getIntegerCodeForString("name");
|
||||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.mp4;
|
|||||||
|
|
||||||
import static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType;
|
import static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
@ -39,7 +40,7 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */
|
/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */
|
||||||
@SuppressWarnings("ConstantField")
|
@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"})
|
||||||
/* package */ final class AtomParsers {
|
/* package */ final class AtomParsers {
|
||||||
|
|
||||||
private static final String TAG = "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_subt = Util.getIntegerCodeForString("subt");
|
||||||
private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
|
private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
|
||||||
private static final int TYPE_meta = Util.getIntegerCodeForString("meta");
|
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
|
* 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)
|
DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime)
|
||||||
throws ParserException {
|
throws ParserException {
|
||||||
Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
|
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) {
|
if (trackType == C.TRACK_TYPE_UNKNOWN) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -485,6 +487,7 @@ import java.util.List;
|
|||||||
* @param isQuickTime True for QuickTime media. False otherwise.
|
* @param isQuickTime True for QuickTime media. False otherwise.
|
||||||
* @return Parsed metadata, or null.
|
* @return Parsed metadata, or null.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
|
public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
|
||||||
if (isQuickTime) {
|
if (isQuickTime) {
|
||||||
// Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
|
// 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();
|
int atomType = udtaData.readInt();
|
||||||
if (atomType == Atom.TYPE_meta) {
|
if (atomType == Atom.TYPE_meta) {
|
||||||
udtaData.setPosition(atomPosition);
|
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;
|
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);
|
meta.skipBytes(Atom.FULL_HEADER_SIZE);
|
||||||
while (meta.getPosition() < limit) {
|
while (meta.getPosition() < limit) {
|
||||||
int atomPosition = meta.getPosition();
|
int atomPosition = meta.getPosition();
|
||||||
@ -516,11 +574,12 @@ import java.util.List;
|
|||||||
meta.setPosition(atomPosition);
|
meta.setPosition(atomPosition);
|
||||||
return parseIlst(meta, atomPosition + atomSize);
|
return parseIlst(meta, atomPosition + atomSize);
|
||||||
}
|
}
|
||||||
meta.skipBytes(atomSize - Atom.HEADER_SIZE);
|
meta.setPosition(atomPosition + atomSize);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
|
private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
|
||||||
ilst.skipBytes(Atom.HEADER_SIZE);
|
ilst.skipBytes(Atom.HEADER_SIZE);
|
||||||
ArrayList<Metadata.Entry> entries = new ArrayList<>();
|
ArrayList<Metadata.Entry> entries = new ArrayList<>();
|
||||||
@ -610,19 +669,22 @@ import java.util.List;
|
|||||||
* Parses an hdlr atom.
|
* Parses an hdlr atom.
|
||||||
*
|
*
|
||||||
* @param hdlr The hdlr atom to decode.
|
* @param hdlr The hdlr atom to decode.
|
||||||
* @return The track type.
|
* @return The handler value.
|
||||||
*/
|
*/
|
||||||
private static int parseHdlr(ParsableByteArray hdlr) {
|
private static int parseHdlr(ParsableByteArray hdlr) {
|
||||||
hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
|
hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
|
||||||
int trackType = hdlr.readInt();
|
return hdlr.readInt();
|
||||||
if (trackType == TYPE_soun) {
|
}
|
||||||
|
|
||||||
|
/** Returns the track type for a given handler value. */
|
||||||
|
private static int getTrackTypeForHdlr(int hdlr) {
|
||||||
|
if (hdlr == TYPE_soun) {
|
||||||
return C.TRACK_TYPE_AUDIO;
|
return C.TRACK_TYPE_AUDIO;
|
||||||
} else if (trackType == TYPE_vide) {
|
} else if (hdlr == TYPE_vide) {
|
||||||
return C.TRACK_TYPE_VIDEO;
|
return C.TRACK_TYPE_VIDEO;
|
||||||
} else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt
|
} else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) {
|
||||||
|| trackType == TYPE_clcp) {
|
|
||||||
return C.TRACK_TYPE_TEXT;
|
return C.TRACK_TYPE_TEXT;
|
||||||
} else if (trackType == TYPE_meta) {
|
} else if (hdlr == TYPE_meta) {
|
||||||
return C.TRACK_TYPE_METADATA;
|
return C.TRACK_TYPE_METADATA;
|
||||||
} else {
|
} else {
|
||||||
return C.TRACK_TYPE_UNKNOWN;
|
return C.TRACK_TYPE_UNKNOWN;
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -16,6 +16,9 @@
|
|||||||
package com.google.android.exoplayer2.extractor.mp4;
|
package com.google.android.exoplayer2.extractor.mp4;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
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.Metadata;
|
||||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
||||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
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.Log;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
/**
|
/** Utilities for handling metadata in MP4. */
|
||||||
* Parses metadata items stored in ilst atoms.
|
|
||||||
*/
|
|
||||||
/* package */ final class MetadataUtil {
|
/* package */ final class MetadataUtil {
|
||||||
|
|
||||||
private static final String TAG = "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 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() {}
|
private MetadataUtil() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting
|
* Returns a {@link Format} that is the same as the input format but includes information from the
|
||||||
* from the current position of the {@link ParsableByteArray}, and the position is advanced by the
|
* specified sources of metadata.
|
||||||
* size of the element. The position is advanced even if the element's type is unrecognized.
|
*/
|
||||||
|
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.
|
* @param ilst Holds the data to be parsed.
|
||||||
* @return The parsed element, or null if the element's type was not recognized.
|
* @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 position = ilst.getPosition();
|
||||||
int endPosition = position + ilst.readInt();
|
int endPosition = position + ilst.readInt();
|
||||||
int type = ilst.readInt();
|
int type = ilst.readInt();
|
||||||
int typeTopByte = (type >> 24) & 0xFF;
|
int typeTopByte = (type >> 24) & 0xFF;
|
||||||
try {
|
try {
|
||||||
if (typeTopByte == '\u00A9' /* Copyright char */
|
if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) {
|
||||||
|| typeTopByte == '\uFFFD' /* Replacement char */) {
|
|
||||||
int shortType = type & 0x00FFFFFF;
|
int shortType = type & 0x00FFFFFF;
|
||||||
if (shortType == SHORT_TYPE_COMMENT) {
|
if (shortType == SHORT_TYPE_COMMENT) {
|
||||||
return parseCommentAttribute(type, ilst);
|
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 type, String id, ParsableByteArray data) {
|
||||||
int atomSize = data.readInt();
|
int atomSize = data.readInt();
|
||||||
int atomType = data.readInt();
|
int atomType = data.readInt();
|
||||||
@ -198,7 +278,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
return null;
|
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 atomSize = data.readInt();
|
||||||
int atomType = data.readInt();
|
int atomType = data.readInt();
|
||||||
if (atomType == Atom.TYPE_data) {
|
if (atomType == Atom.TYPE_data) {
|
||||||
@ -210,7 +291,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @Nullable Id3Frame parseUint8Attribute(
|
@Nullable
|
||||||
|
private static Id3Frame parseUint8Attribute(
|
||||||
int type,
|
int type,
|
||||||
String id,
|
String id,
|
||||||
ParsableByteArray data,
|
ParsableByteArray data,
|
||||||
@ -229,7 +311,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @Nullable TextInformationFrame parseIndexAndCountAttribute(
|
@Nullable
|
||||||
|
private static TextInformationFrame parseIndexAndCountAttribute(
|
||||||
int type, String attributeName, ParsableByteArray data) {
|
int type, String attributeName, ParsableByteArray data) {
|
||||||
int atomSize = data.readInt();
|
int atomSize = data.readInt();
|
||||||
int atomType = data.readInt();
|
int atomType = data.readInt();
|
||||||
@ -249,8 +332,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @Nullable TextInformationFrame parseStandardGenreAttribute(
|
@Nullable
|
||||||
ParsableByteArray data) {
|
private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {
|
||||||
int genreCode = parseUint8AttributeValue(data);
|
int genreCode = parseUint8AttributeValue(data);
|
||||||
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
|
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
|
||||||
? STANDARD_GENRES[genreCode - 1] : null;
|
? STANDARD_GENRES[genreCode - 1] : null;
|
||||||
@ -261,7 +344,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @Nullable ApicFrame parseCoverArt(ParsableByteArray data) {
|
@Nullable
|
||||||
|
private static ApicFrame parseCoverArt(ParsableByteArray data) {
|
||||||
int atomSize = data.readInt();
|
int atomSize = data.readInt();
|
||||||
int atomType = data.readInt();
|
int atomType = data.readInt();
|
||||||
if (atomType == Atom.TYPE_data) {
|
if (atomType == Atom.TYPE_data) {
|
||||||
@ -285,8 +369,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @Nullable Id3Frame parseInternalAttribute(
|
@Nullable
|
||||||
ParsableByteArray data, int endPosition) {
|
private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) {
|
||||||
String domain = null;
|
String domain = null;
|
||||||
String name = null;
|
String name = null;
|
||||||
int dataAtomPosition = -1;
|
int dataAtomPosition = -1;
|
||||||
|
@ -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_ATOM_PAYLOAD = 1;
|
||||||
private static final int STATE_READING_SAMPLE = 2;
|
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 ");
|
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;
|
long durationUs = C.TIME_UNSET;
|
||||||
List<Mp4Track> tracks = new ArrayList<>();
|
List<Mp4Track> tracks = new ArrayList<>();
|
||||||
|
|
||||||
Metadata metadata = null;
|
// Process metadata.
|
||||||
|
Metadata udtaMetadata = null;
|
||||||
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
|
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
|
||||||
Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
|
Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
|
||||||
if (udta != null) {
|
if (udta != null) {
|
||||||
metadata = AtomParsers.parseUdta(udta, isQuickTime);
|
udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime);
|
||||||
if (metadata != null) {
|
if (udtaMetadata != null) {
|
||||||
gaplessInfoHolder.setFromMetadata(metadata);
|
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;
|
boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
|
||||||
ArrayList<TrackSampleTable> trackSampleTables =
|
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.
|
// Allow ten source samples per output sample, like the platform extractor.
|
||||||
int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
|
int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
|
||||||
Format format = track.format.copyWithMaxInputSize(maxInputSize);
|
Format format = track.format.copyWithMaxInputSize(maxInputSize);
|
||||||
if (track.type == C.TRACK_TYPE_AUDIO) {
|
format =
|
||||||
if (gaplessInfoHolder.hasGaplessInfo()) {
|
MetadataUtil.getFormatWithMetadata(
|
||||||
format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
|
track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder);
|
||||||
gaplessInfoHolder.encoderPadding);
|
|
||||||
}
|
|
||||||
if (metadata != null) {
|
|
||||||
format = format.copyWithMetadata(metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mp4Track.trackOutput.format(format);
|
mp4Track.trackOutput.format(format);
|
||||||
|
|
||||||
durationUs =
|
durationUs =
|
||||||
@ -716,24 +716,37 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||||||
return false;
|
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) {
|
private static boolean shouldParseLeafAtom(int atom) {
|
||||||
return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr
|
return atom == Atom.TYPE_mdhd
|
||||||
|| atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss
|
|| atom == Atom.TYPE_mvhd
|
||||||
|| atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc
|
|| atom == Atom.TYPE_hdlr
|
||||||
|| atom == Atom.TYPE_stsz || atom == Atom.TYPE_stz2 || atom == Atom.TYPE_stco
|
|| atom == Atom.TYPE_stsd
|
||||||
|| atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp
|
|| atom == Atom.TYPE_stts
|
||||||
|| atom == Atom.TYPE_udta;
|
|| 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) {
|
private static boolean shouldParseContainerAtom(int atom) {
|
||||||
return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
|
return atom == Atom.TYPE_moov
|
||||||
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_edts;
|
|| 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 {
|
private static final class Mp4Track {
|
||||||
|
@ -27,9 +27,7 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
/* package */ final class Sniffer {
|
/* 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 SEARCH_LENGTH = 4 * 1024;
|
||||||
|
|
||||||
private static final int[] COMPATIBLE_BRANDS = new int[] {
|
private static final int[] COMPATIBLE_BRANDS = new int[] {
|
||||||
@ -109,15 +107,19 @@ import java.io.IOException;
|
|||||||
headerSize = Atom.LONG_HEADER_SIZE;
|
headerSize = Atom.LONG_HEADER_SIZE;
|
||||||
input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
|
input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
|
||||||
buffer.setLimit(Atom.LONG_HEADER_SIZE);
|
buffer.setLimit(Atom.LONG_HEADER_SIZE);
|
||||||
atomSize = buffer.readUnsignedLongToLong();
|
atomSize = buffer.readLong();
|
||||||
} else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {
|
} else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {
|
||||||
// The atom extends to the end of the file.
|
// The atom extends to the end of the file.
|
||||||
long endPosition = input.getLength();
|
long fileEndPosition = input.getLength();
|
||||||
if (endPosition != C.LENGTH_UNSET) {
|
if (fileEndPosition != C.LENGTH_UNSET) {
|
||||||
atomSize = endPosition - input.getPosition() + headerSize;
|
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) {
|
if (atomSize < headerSize) {
|
||||||
// The file is invalid because the atom size is too small for its header.
|
// The file is invalid because the atom size is too small for its header.
|
||||||
return false;
|
return false;
|
||||||
@ -125,6 +127,13 @@ import java.io.IOException;
|
|||||||
bytesSearched += headerSize;
|
bytesSearched += headerSize;
|
||||||
|
|
||||||
if (atomType == Atom.TYPE_moov) {
|
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.
|
// Check for an mvex atom inside the moov atom to identify whether the file is fragmented.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,9 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
this.durationUs = durationUs;
|
this.durationUs = durationUs;
|
||||||
sampleCount = offsets.length;
|
sampleCount = offsets.length;
|
||||||
|
if (flags.length > 0) {
|
||||||
|
flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.ts;
|
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.C;
|
||||||
import com.google.android.exoplayer2.audio.Ac3Util;
|
import com.google.android.exoplayer2.audio.Ac3Util;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
@ -140,7 +142,7 @@ public final class Ac3Extractor implements Extractor {
|
|||||||
|
|
||||||
if (!startedPacket) {
|
if (!startedPacket) {
|
||||||
// Pass data to the reader as though it's contained within a single infinitely long packet.
|
// 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;
|
startedPacket = true;
|
||||||
}
|
}
|
||||||
// TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes
|
// TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes
|
||||||
|
@ -100,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
|
||||||
timeUs = pesTimeUs;
|
timeUs = pesTimeUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.ts;
|
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.IntDef;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
@ -202,7 +204,7 @@ public final class AdtsExtractor implements Extractor {
|
|||||||
|
|
||||||
if (!startedPacket) {
|
if (!startedPacket) {
|
||||||
// Pass data to the reader as though it's contained within a single infinitely long packet.
|
// 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;
|
startedPacket = true;
|
||||||
}
|
}
|
||||||
// TODO: Make it possible for reader to consume the dataSource directly, so that it becomes
|
// TODO: Make it possible for reader to consume the dataSource directly, so that it becomes
|
||||||
|
@ -141,7 +141,7 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
|
||||||
timeUs = pesTimeUs;
|
timeUs = pesTimeUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +50,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||||||
FLAG_IGNORE_H264_STREAM,
|
FLAG_IGNORE_H264_STREAM,
|
||||||
FLAG_DETECT_ACCESS_UNITS,
|
FLAG_DETECT_ACCESS_UNITS,
|
||||||
FLAG_IGNORE_SPLICE_INFO_STREAM,
|
FLAG_IGNORE_SPLICE_INFO_STREAM,
|
||||||
FLAG_OVERRIDE_CAPTION_DESCRIPTORS
|
FLAG_OVERRIDE_CAPTION_DESCRIPTORS,
|
||||||
|
FLAG_IGNORE_HDMV_DTS_STREAM
|
||||||
})
|
})
|
||||||
public @interface Flags {}
|
public @interface Flags {}
|
||||||
|
|
||||||
@ -86,6 +87,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||||||
* closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors.
|
* closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors.
|
||||||
*/
|
*/
|
||||||
public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5;
|
public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5;
|
||||||
|
/**
|
||||||
|
* Prevents the creation of {@link DtsReader} instances when receiving {@link
|
||||||
|
* TsExtractor#TS_STREAM_TYPE_HDMV_DTS} as stream type. Enabling this flag prevents a stream type
|
||||||
|
* collision between HDMV DTS audio and SCTE-35 subtitles.
|
||||||
|
*/
|
||||||
|
public static final int FLAG_IGNORE_HDMV_DTS_STREAM = 1 << 6;
|
||||||
|
|
||||||
private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86;
|
private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86;
|
||||||
|
|
||||||
@ -142,8 +149,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||||||
case TsExtractor.TS_STREAM_TYPE_AC3:
|
case TsExtractor.TS_STREAM_TYPE_AC3:
|
||||||
case TsExtractor.TS_STREAM_TYPE_E_AC3:
|
case TsExtractor.TS_STREAM_TYPE_E_AC3:
|
||||||
return new PesReader(new Ac3Reader(esInfo.language));
|
return new PesReader(new Ac3Reader(esInfo.language));
|
||||||
case TsExtractor.TS_STREAM_TYPE_DTS:
|
|
||||||
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
|
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
|
||||||
|
if (isSet(FLAG_IGNORE_HDMV_DTS_STREAM)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Fall through.
|
||||||
|
case TsExtractor.TS_STREAM_TYPE_DTS:
|
||||||
return new PesReader(new DtsReader(esInfo.language));
|
return new PesReader(new DtsReader(esInfo.language));
|
||||||
case TsExtractor.TS_STREAM_TYPE_H262:
|
case TsExtractor.TS_STREAM_TYPE_H262:
|
||||||
return new PesReader(new H262Reader(buildUserDataReader(esInfo)));
|
return new PesReader(new H262Reader(buildUserDataReader(esInfo)));
|
||||||
|
@ -80,7 +80,7 @@ public final class DtsReader implements ElementaryStreamReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
|
||||||
timeUs = pesTimeUs;
|
timeUs = pesTimeUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.ts;
|
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.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
@ -73,8 +75,8 @@ public final class DvbSubtitleReader implements ElementaryStreamReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
|
||||||
if (!dataAlignmentIndicator) {
|
if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
writingSample = true;
|
writingSample = true;
|
||||||
|
@ -43,9 +43,9 @@ public interface ElementaryStreamReader {
|
|||||||
* Called when a packet starts.
|
* Called when a packet starts.
|
||||||
*
|
*
|
||||||
* @param pesTimeUs The timestamp associated with the packet.
|
* @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.
|
* Consumes (possibly partial) data from the current packet.
|
||||||
|
@ -107,7 +107,8 @@ public final class H262Reader implements ElementaryStreamReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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;
|
this.pesTimeUs = pesTimeUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.ts;
|
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 android.util.SparseArray;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
@ -56,9 +58,12 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||||||
// State that should not be reset on seek.
|
// State that should not be reset on seek.
|
||||||
private boolean hasOutputFormat;
|
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;
|
private long pesTimeUs;
|
||||||
|
|
||||||
|
// State inherited from the TS packet header.
|
||||||
|
private boolean randomAccessIndicator;
|
||||||
|
|
||||||
// Scratch variables to avoid allocations.
|
// Scratch variables to avoid allocations.
|
||||||
private final ParsableByteArray seiWrapper;
|
private final ParsableByteArray seiWrapper;
|
||||||
|
|
||||||
@ -88,6 +93,7 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||||||
sei.reset();
|
sei.reset();
|
||||||
sampleReader.reset();
|
sampleReader.reset();
|
||||||
totalBytesWritten = 0;
|
totalBytesWritten = 0;
|
||||||
|
randomAccessIndicator = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -100,8 +106,9 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
|
||||||
this.pesTimeUs = pesTimeUs;
|
this.pesTimeUs = pesTimeUs;
|
||||||
|
randomAccessIndicator |= (flags & FLAG_RANDOM_ACCESS_INDICATOR) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -220,12 +227,17 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||||||
seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.
|
seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.
|
||||||
seiReader.consume(pesTimeUs, seiWrapper);
|
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 class SampleReader {
|
||||||
|
|
||||||
private static final int DEFAULT_BUFFER_SIZE = 128;
|
private static final int DEFAULT_BUFFER_SIZE = 128;
|
||||||
@ -430,11 +442,12 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||||||
isFilling = false;
|
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
|
if (nalUnitType == NAL_UNIT_TYPE_AUD
|
||||||
|| (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) {
|
|| (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) {
|
||||||
// If the NAL unit ending is the start of a new sample, output the previous one.
|
// 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);
|
int nalUnitLength = (int) (position - nalUnitStartPosition);
|
||||||
outputSample(offset + nalUnitLength);
|
outputSample(offset + nalUnitLength);
|
||||||
}
|
}
|
||||||
@ -443,8 +456,12 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||||||
sampleIsKeyframe = false;
|
sampleIsKeyframe = false;
|
||||||
readingSample = true;
|
readingSample = true;
|
||||||
}
|
}
|
||||||
sampleIsKeyframe |= nalUnitType == NAL_UNIT_TYPE_IDR || (allowNonIdrKeyframes
|
boolean treatIFrameAsKeyframe =
|
||||||
&& nalUnitType == NAL_UNIT_TYPE_NON_IDR && sliceHeader.isISlice());
|
allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator;
|
||||||
|
sampleIsKeyframe |=
|
||||||
|
nalUnitType == NAL_UNIT_TYPE_IDR
|
||||||
|
|| (treatIFrameAsKeyframe && nalUnitType == NAL_UNIT_TYPE_NON_IDR);
|
||||||
|
return sampleIsKeyframe;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void outputSample(int offset) {
|
private void outputSample(int offset) {
|
||||||
@ -486,10 +503,21 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||||||
hasSliceType = true;
|
hasSliceType = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAll(SpsData spsData, int nalRefIdc, int sliceType, int frameNum,
|
public void setAll(
|
||||||
int picParameterSetId, boolean fieldPicFlag, boolean bottomFieldFlagPresent,
|
SpsData spsData,
|
||||||
boolean bottomFieldFlag, boolean idrPicFlag, int idrPicId, int picOrderCntLsb,
|
int nalRefIdc,
|
||||||
int deltaPicOrderCntBottom, int deltaPicOrderCnt0, int deltaPicOrderCnt1) {
|
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.spsData = spsData;
|
||||||
this.nalRefIdc = nalRefIdc;
|
this.nalRefIdc = nalRefIdc;
|
||||||
this.sliceType = sliceType;
|
this.sliceType = sliceType;
|
||||||
@ -514,23 +542,26 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||||||
|
|
||||||
private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) {
|
private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) {
|
||||||
// See ISO 14496-10 subsection 7.4.1.2.4.
|
// See ISO 14496-10 subsection 7.4.1.2.4.
|
||||||
return isComplete && (!other.isComplete || frameNum != other.frameNum
|
return isComplete
|
||||||
|| picParameterSetId != other.picParameterSetId || fieldPicFlag != other.fieldPicFlag
|
&& (!other.isComplete
|
||||||
|| (bottomFieldFlagPresent && other.bottomFieldFlagPresent
|
|| frameNum != other.frameNum
|
||||||
&& bottomFieldFlag != other.bottomFieldFlag)
|
|| picParameterSetId != other.picParameterSetId
|
||||||
|| (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
|
|| fieldPicFlag != other.fieldPicFlag
|
||||||
|| (spsData.picOrderCountType == 0 && other.spsData.picOrderCountType == 0
|
|| (bottomFieldFlagPresent
|
||||||
&& (picOrderCntLsb != other.picOrderCntLsb
|
&& other.bottomFieldFlagPresent
|
||||||
|| deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))
|
&& bottomFieldFlag != other.bottomFieldFlag)
|
||||||
|| (spsData.picOrderCountType == 1 && other.spsData.picOrderCountType == 1
|
|| (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
|
||||||
&& (deltaPicOrderCnt0 != other.deltaPicOrderCnt0
|
|| (spsData.picOrderCountType == 0
|
||||||
|| deltaPicOrderCnt1 != other.deltaPicOrderCnt1))
|
&& other.spsData.picOrderCountType == 0
|
||||||
|| idrPicFlag != other.idrPicFlag
|
&& (picOrderCntLsb != other.picOrderCntLsb
|
||||||
|| (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId));
|
|| 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,8 @@ public final class H265Reader implements ElementaryStreamReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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;
|
this.pesTimeUs = pesTimeUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.ts;
|
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.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
@ -63,8 +65,8 @@ public final class Id3Reader implements ElementaryStreamReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
|
||||||
if (!dataAlignmentIndicator) {
|
if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
writingSample = true;
|
writingSample = true;
|
||||||
|
@ -93,7 +93,7 @@ public final class LatmReader implements ElementaryStreamReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
|
||||||
timeUs = pesTimeUs;
|
timeUs = pesTimeUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ public final class MpegAudioReader implements ElementaryStreamReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
|
||||||
timeUs = pesTimeUs;
|
timeUs = pesTimeUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,9 +78,8 @@ public final class PesReader implements TsPayloadReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator)
|
public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException {
|
||||||
throws ParserException {
|
if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) {
|
||||||
if (payloadUnitStartIndicator) {
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case STATE_FINDING_HEADER:
|
case STATE_FINDING_HEADER:
|
||||||
case STATE_READING_HEADER:
|
case STATE_READING_HEADER:
|
||||||
@ -122,7 +121,8 @@ public final class PesReader implements TsPayloadReader {
|
|||||||
if (continueRead(data, pesScratch.data, readLength)
|
if (continueRead(data, pesScratch.data, readLength)
|
||||||
&& continueRead(data, null, extendedHeaderLength)) {
|
&& continueRead(data, null, extendedHeaderLength)) {
|
||||||
parseHeaderExtension();
|
parseHeaderExtension();
|
||||||
reader.packetStarted(timeUs, dataAlignmentIndicator);
|
flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;
|
||||||
|
reader.packetStarted(timeUs, flags);
|
||||||
setState(STATE_READING_BODY);
|
setState(STATE_READING_BODY);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -343,7 +343,7 @@ public final class PsExtractor implements Extractor {
|
|||||||
data.readBytes(pesScratch.data, 0, extendedHeaderLength);
|
data.readBytes(pesScratch.data, 0, extendedHeaderLength);
|
||||||
pesScratch.setPosition(0);
|
pesScratch.setPosition(0);
|
||||||
parseHeaderExtension();
|
parseHeaderExtension();
|
||||||
pesPayloadReader.packetStarted(timeUs, true);
|
pesPayloadReader.packetStarted(timeUs, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR);
|
||||||
pesPayloadReader.consume(data);
|
pesPayloadReader.consume(data);
|
||||||
// We always have complete PES packets with program stream.
|
// We always have complete PES packets with program stream.
|
||||||
pesPayloadReader.packetFinished();
|
pesPayloadReader.packetFinished();
|
||||||
|
@ -57,7 +57,8 @@ public final class SectionReader implements TsPayloadReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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;
|
int payloadStartPosition = C.POSITION_UNSET;
|
||||||
if (payloadUnitStartIndicator) {
|
if (payloadUnitStartIndicator) {
|
||||||
int payloadStartOffset = data.readUnsignedByte();
|
int payloadStartOffset = data.readUnsignedByte();
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.ts;
|
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.support.annotation.IntDef;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.util.SparseBooleanArray;
|
import android.util.SparseBooleanArray;
|
||||||
@ -279,6 +281,8 @@ public final class TsExtractor implements Extractor {
|
|||||||
return RESULT_CONTINUE;
|
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.
|
// Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
|
||||||
int tsPacketHeader = tsPacketBuffer.readInt();
|
int tsPacketHeader = tsPacketBuffer.readInt();
|
||||||
if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator
|
if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator
|
||||||
@ -286,7 +290,7 @@ public final class TsExtractor implements Extractor {
|
|||||||
tsPacketBuffer.setPosition(endOfPacket);
|
tsPacketBuffer.setPosition(endOfPacket);
|
||||||
return RESULT_CONTINUE;
|
return RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
boolean payloadUnitStartIndicator = (tsPacketHeader & 0x400000) != 0;
|
packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0;
|
||||||
// Ignoring transport_priority (tsPacketHeader & 0x200000)
|
// Ignoring transport_priority (tsPacketHeader & 0x200000)
|
||||||
int pid = (tsPacketHeader & 0x1FFF00) >> 8;
|
int pid = (tsPacketHeader & 0x1FFF00) >> 8;
|
||||||
// Ignoring transport_scrambling_control (tsPacketHeader & 0xC0)
|
// Ignoring transport_scrambling_control (tsPacketHeader & 0xC0)
|
||||||
@ -317,14 +321,20 @@ public final class TsExtractor implements Extractor {
|
|||||||
// Skip the adaptation field.
|
// Skip the adaptation field.
|
||||||
if (adaptationFieldExists) {
|
if (adaptationFieldExists) {
|
||||||
int adaptationFieldLength = tsPacketBuffer.readUnsignedByte();
|
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.
|
// Read the payload.
|
||||||
boolean wereTracksEnded = tracksEnded;
|
boolean wereTracksEnded = tracksEnded;
|
||||||
if (shouldConsumePacketPayload(pid)) {
|
if (shouldConsumePacketPayload(pid)) {
|
||||||
tsPacketBuffer.setLimit(endOfPacket);
|
tsPacketBuffer.setLimit(endOfPacket);
|
||||||
payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator);
|
payloadReader.consume(tsPacketBuffer, packetHeaderFlags);
|
||||||
tsPacketBuffer.setLimit(limit);
|
tsPacketBuffer.setLimit(limit);
|
||||||
}
|
}
|
||||||
if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) {
|
if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) {
|
||||||
|
@ -15,12 +15,16 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.extractor.ts;
|
package com.google.android.exoplayer2.extractor.ts;
|
||||||
|
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
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.Collections;
|
||||||
import java.util.List;
|
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.
|
* Initializes the payload reader.
|
||||||
*
|
*
|
||||||
@ -187,10 +214,10 @@ public interface TsPayloadReader {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies the reader that a seek has occurred.
|
* Notifies the reader that a seek has occurred.
|
||||||
* <p>
|
*
|
||||||
* Following a call to this method, the data passed to the next invocation of
|
* <p>Following a call to this method, the data passed to the next invocation of {@link #consume}
|
||||||
* {@link #consume(ParsableByteArray, boolean)} will not be a continuation of the data that was
|
* will not be a continuation of the data that was previously passed. Hence the reader should
|
||||||
* previously passed. Hence the reader should reset any internal state.
|
* reset any internal state.
|
||||||
*/
|
*/
|
||||||
void seek();
|
void seek();
|
||||||
|
|
||||||
@ -198,9 +225,8 @@ public interface TsPayloadReader {
|
|||||||
* Consumes the payload of a TS packet.
|
* Consumes the payload of a TS packet.
|
||||||
*
|
*
|
||||||
* @param data The TS packet. The position will be set to the start of the payload.
|
* @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.
|
* @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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -248,9 +248,15 @@ public final class MediaCodecInfo {
|
|||||||
// If we don't know any better, we assume that the profile and level are supported.
|
// If we don't know any better, we assume that the profile and level are supported.
|
||||||
return true;
|
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()) {
|
for (CodecProfileLevel capabilities : getProfileLevels()) {
|
||||||
if (capabilities.profile == codecProfileAndLevel.first
|
if (capabilities.profile == profile && capabilities.level >= level) {
|
||||||
&& capabilities.level >= codecProfileAndLevel.second) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import android.media.MediaCodec;
|
|||||||
import android.media.MediaCodec.CodecException;
|
import android.media.MediaCodec.CodecException;
|
||||||
import android.media.MediaCodec.CryptoException;
|
import android.media.MediaCodec.CryptoException;
|
||||||
import android.media.MediaCrypto;
|
import android.media.MediaCrypto;
|
||||||
|
import android.media.MediaCryptoException;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
@ -239,14 +240,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
|
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({DRAIN_ACTION_NONE, DRAIN_ACTION_FLUSH, DRAIN_ACTION_REINITIALIZE})
|
@IntDef({
|
||||||
|
DRAIN_ACTION_NONE,
|
||||||
|
DRAIN_ACTION_FLUSH,
|
||||||
|
DRAIN_ACTION_UPDATE_DRM_SESSION,
|
||||||
|
DRAIN_ACTION_REINITIALIZE
|
||||||
|
})
|
||||||
private @interface DrainAction {}
|
private @interface DrainAction {}
|
||||||
/** No special action should be taken. */
|
/** No special action should be taken. */
|
||||||
private static final int DRAIN_ACTION_NONE = 0;
|
private static final int DRAIN_ACTION_NONE = 0;
|
||||||
/** The codec should be flushed. */
|
/** The codec should be flushed. */
|
||||||
private static final int DRAIN_ACTION_FLUSH = 1;
|
private static final int DRAIN_ACTION_FLUSH = 1;
|
||||||
/** The codec should be re-initialized. */
|
/** The codec should be flushed and updated to use the pending DRM session. */
|
||||||
private static final int DRAIN_ACTION_REINITIALIZE = 2;
|
private static final int DRAIN_ACTION_UPDATE_DRM_SESSION = 2;
|
||||||
|
/** The codec should be reinitialized. */
|
||||||
|
private static final int DRAIN_ACTION_REINITIALIZE = 3;
|
||||||
|
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@ -287,13 +295,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
private final DecoderInputBuffer flagsOnlyBuffer;
|
private final DecoderInputBuffer flagsOnlyBuffer;
|
||||||
private final FormatHolder formatHolder;
|
private final FormatHolder formatHolder;
|
||||||
private final TimedValueQueue<Format> formatQueue;
|
private final TimedValueQueue<Format> formatQueue;
|
||||||
private final List<Long> decodeOnlyPresentationTimestamps;
|
private final ArrayList<Long> decodeOnlyPresentationTimestamps;
|
||||||
private final MediaCodec.BufferInfo outputBufferInfo;
|
private final MediaCodec.BufferInfo outputBufferInfo;
|
||||||
|
|
||||||
@Nullable private Format inputFormat;
|
@Nullable private Format inputFormat;
|
||||||
private Format outputFormat;
|
private Format outputFormat;
|
||||||
private DrmSession<FrameworkMediaCrypto> drmSession;
|
@Nullable private DrmSession<FrameworkMediaCrypto> codecDrmSession;
|
||||||
private DrmSession<FrameworkMediaCrypto> pendingDrmSession;
|
@Nullable private DrmSession<FrameworkMediaCrypto> sourceDrmSession;
|
||||||
|
@Nullable private MediaCrypto mediaCrypto;
|
||||||
|
private boolean mediaCryptoRequiresSecureDecoder;
|
||||||
private long renderTimeLimitMs;
|
private long renderTimeLimitMs;
|
||||||
private float rendererOperatingRate;
|
private float rendererOperatingRate;
|
||||||
@Nullable private MediaCodec codec;
|
@Nullable private MediaCodec codec;
|
||||||
@ -457,29 +467,36 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
drmSession = pendingDrmSession;
|
setCodecDrmSession(sourceDrmSession);
|
||||||
|
|
||||||
String mimeType = inputFormat.sampleMimeType;
|
String mimeType = inputFormat.sampleMimeType;
|
||||||
MediaCrypto wrappedMediaCrypto = null;
|
if (codecDrmSession != null) {
|
||||||
boolean drmSessionRequiresSecureDecoder = false;
|
|
||||||
if (drmSession != null) {
|
|
||||||
FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto();
|
|
||||||
if (mediaCrypto == null) {
|
if (mediaCrypto == null) {
|
||||||
DrmSessionException drmError = drmSession.getError();
|
FrameworkMediaCrypto sessionMediaCrypto = codecDrmSession.getMediaCrypto();
|
||||||
if (drmError != null) {
|
if (sessionMediaCrypto == null) {
|
||||||
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
|
DrmSessionException drmError = codecDrmSession.getError();
|
||||||
// input format causes the session to be replaced before it's used.
|
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 {
|
} else {
|
||||||
// The drm session isn't open yet.
|
try {
|
||||||
return;
|
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()) {
|
if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
|
||||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
@DrmSession.State int drmSessionState = codecDrmSession.getState();
|
||||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
|
||||||
} else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
|
} else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
|
||||||
// Wait for keys.
|
// Wait for keys.
|
||||||
return;
|
return;
|
||||||
@ -488,7 +505,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
maybeInitCodecWithFallback(wrappedMediaCrypto, drmSessionRequiresSecureDecoder);
|
maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder);
|
||||||
} catch (DecoderInitializationException e) {
|
} catch (DecoderInitializationException e) {
|
||||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||||
}
|
}
|
||||||
@ -537,7 +554,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
||||||
inputStreamEnded = false;
|
inputStreamEnded = false;
|
||||||
outputStreamEnded = false;
|
outputStreamEnded = false;
|
||||||
flushOrReinitCodec();
|
flushOrReinitializeCodec();
|
||||||
formatQueue.clear();
|
formatQueue.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,7 +569,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
@Override
|
@Override
|
||||||
protected void onDisabled() {
|
protected void onDisabled() {
|
||||||
inputFormat = null;
|
inputFormat = null;
|
||||||
if (drmSession != null || pendingDrmSession != null) {
|
if (sourceDrmSession != null || codecDrmSession != null) {
|
||||||
// TODO: Do something better with this case.
|
// TODO: Do something better with this case.
|
||||||
onReset();
|
onReset();
|
||||||
} else {
|
} else {
|
||||||
@ -565,51 +582,40 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
try {
|
try {
|
||||||
releaseCodec();
|
releaseCodec();
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
setSourceDrmSession(null);
|
||||||
if (drmSession != null) {
|
|
||||||
drmSessionManager.releaseSession(drmSession);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
|
|
||||||
drmSessionManager.releaseSession(pendingDrmSession);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
drmSession = null;
|
|
||||||
pendingDrmSession = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void releaseCodec() {
|
protected void releaseCodec() {
|
||||||
availableCodecInfos = null;
|
availableCodecInfos = null;
|
||||||
if (codec != null) {
|
codecInfo = null;
|
||||||
codecInfo = null;
|
codecFormat = null;
|
||||||
codecFormat = null;
|
resetInputBuffer();
|
||||||
resetInputBuffer();
|
resetOutputBuffer();
|
||||||
resetOutputBuffer();
|
resetCodecBuffers();
|
||||||
resetCodecBuffers();
|
waitingForKeys = false;
|
||||||
waitingForKeys = false;
|
codecHotswapDeadlineMs = C.TIME_UNSET;
|
||||||
codecHotswapDeadlineMs = C.TIME_UNSET;
|
decodeOnlyPresentationTimestamps.clear();
|
||||||
decodeOnlyPresentationTimestamps.clear();
|
try {
|
||||||
decoderCounters.decoderReleaseCount++;
|
if (codec != null) {
|
||||||
try {
|
decoderCounters.decoderReleaseCount++;
|
||||||
codec.stop();
|
|
||||||
} finally {
|
|
||||||
try {
|
try {
|
||||||
codec.release();
|
codec.stop();
|
||||||
} finally {
|
} finally {
|
||||||
codec = null;
|
codec.release();
|
||||||
if (drmSession != null && pendingDrmSession != drmSession) {
|
|
||||||
try {
|
|
||||||
drmSessionManager.releaseSession(drmSession);
|
|
||||||
} finally {
|
|
||||||
drmSession = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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
|
* <p>The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link
|
||||||
* #maybeInitCodec()} if the codec needs to be re-instantiated.
|
* #maybeInitCodec()} if the codec needs to be re-instantiated.
|
||||||
*
|
*
|
||||||
|
* @return Whether the codec was released and reinitialized, rather than being flushed.
|
||||||
* @throws ExoPlaybackException If an error occurs re-instantiating the codec.
|
* @throws ExoPlaybackException If an error occurs re-instantiating the codec.
|
||||||
*/
|
*/
|
||||||
protected final void flushOrReinitCodec() throws ExoPlaybackException {
|
protected final boolean flushOrReinitializeCodec() throws ExoPlaybackException {
|
||||||
if (flushOrReleaseCodec()) {
|
boolean released = flushOrReleaseCodec();
|
||||||
|
if (released) {
|
||||||
maybeInitCodec();
|
maybeInitCodec();
|
||||||
}
|
}
|
||||||
|
return released;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -729,18 +738,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void maybeInitCodecWithFallback(
|
private void maybeInitCodecWithFallback(
|
||||||
MediaCrypto crypto, boolean drmSessionRequiresSecureDecoder)
|
MediaCrypto crypto, boolean mediaCryptoRequiresSecureDecoder)
|
||||||
throws DecoderInitializationException {
|
throws DecoderInitializationException {
|
||||||
if (availableCodecInfos == null) {
|
if (availableCodecInfos == null) {
|
||||||
try {
|
try {
|
||||||
availableCodecInfos =
|
availableCodecInfos =
|
||||||
new ArrayDeque<>(getAvailableCodecInfos(drmSessionRequiresSecureDecoder));
|
new ArrayDeque<>(getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder));
|
||||||
preferredDecoderInitializationException = null;
|
preferredDecoderInitializationException = null;
|
||||||
} catch (DecoderQueryException e) {
|
} catch (DecoderQueryException e) {
|
||||||
throw new DecoderInitializationException(
|
throw new DecoderInitializationException(
|
||||||
inputFormat,
|
inputFormat,
|
||||||
e,
|
e,
|
||||||
drmSessionRequiresSecureDecoder,
|
mediaCryptoRequiresSecureDecoder,
|
||||||
DecoderInitializationException.DECODER_QUERY_ERROR);
|
DecoderInitializationException.DECODER_QUERY_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -749,7 +758,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
throw new DecoderInitializationException(
|
throw new DecoderInitializationException(
|
||||||
inputFormat,
|
inputFormat,
|
||||||
/* cause= */ null,
|
/* cause= */ null,
|
||||||
drmSessionRequiresSecureDecoder,
|
mediaCryptoRequiresSecureDecoder,
|
||||||
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);
|
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -768,7 +777,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
availableCodecInfos.removeFirst();
|
availableCodecInfos.removeFirst();
|
||||||
DecoderInitializationException exception =
|
DecoderInitializationException exception =
|
||||||
new DecoderInitializationException(
|
new DecoderInitializationException(
|
||||||
inputFormat, e, drmSessionRequiresSecureDecoder, codecInfo.name);
|
inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo.name);
|
||||||
if (preferredDecoderInitializationException == null) {
|
if (preferredDecoderInitializationException == null) {
|
||||||
preferredDecoderInitializationException = exception;
|
preferredDecoderInitializationException = exception;
|
||||||
} else {
|
} else {
|
||||||
@ -784,11 +793,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
availableCodecInfos = null;
|
availableCodecInfos = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<MediaCodecInfo> getAvailableCodecInfos(boolean drmSessionRequiresSecureDecoder)
|
private List<MediaCodecInfo> getAvailableCodecInfos(boolean mediaCryptoRequiresSecureDecoder)
|
||||||
throws DecoderQueryException {
|
throws DecoderQueryException {
|
||||||
List<MediaCodecInfo> codecInfos =
|
List<MediaCodecInfo> codecInfos =
|
||||||
getDecoderInfos(mediaCodecSelector, inputFormat, drmSessionRequiresSecureDecoder);
|
getDecoderInfos(mediaCodecSelector, inputFormat, mediaCryptoRequiresSecureDecoder);
|
||||||
if (codecInfos.isEmpty() && drmSessionRequiresSecureDecoder) {
|
if (codecInfos.isEmpty() && mediaCryptoRequiresSecureDecoder) {
|
||||||
// The drm session indicates that a secure decoder is required, but the device does not
|
// The drm session indicates that a secure decoder is required, but the device does not
|
||||||
// have one. Assuming that supportsFormat indicated support for the media being played, we
|
// have one. Assuming that supportsFormat indicated support for the media being played, we
|
||||||
// know that it does not require a secure output path. Most CDM implementations allow
|
// know that it does not require a secure output path. Most CDM implementations allow
|
||||||
@ -928,6 +937,24 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setSourceDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) {
|
||||||
|
DrmSession<FrameworkMediaCrypto> previous = sourceDrmSession;
|
||||||
|
sourceDrmSession = session;
|
||||||
|
releaseDrmSessionIfUnused(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCodecDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) {
|
||||||
|
DrmSession<FrameworkMediaCrypto> previous = codecDrmSession;
|
||||||
|
codecDrmSession = session;
|
||||||
|
releaseDrmSessionIfUnused(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseDrmSessionIfUnused(@Nullable DrmSession<FrameworkMediaCrypto> session) {
|
||||||
|
if (session != null && session != sourceDrmSession && session != codecDrmSession) {
|
||||||
|
drmSessionManager.releaseSession(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Whether it may be possible to feed more input data.
|
* @return Whether it may be possible to feed more input data.
|
||||||
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
|
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
|
||||||
@ -1082,12 +1109,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||||
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
if (codecDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@DrmSession.State int drmSessionState = drmSession.getState();
|
@DrmSession.State int drmSessionState = codecDrmSession.getState();
|
||||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
|
||||||
}
|
}
|
||||||
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
||||||
}
|
}
|
||||||
@ -1126,13 +1153,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
throw ExoPlaybackException.createForRenderer(
|
throw ExoPlaybackException.createForRenderer(
|
||||||
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
|
||||||
}
|
}
|
||||||
pendingDrmSession =
|
DrmSession<FrameworkMediaCrypto> session =
|
||||||
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
|
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
|
||||||
if (pendingDrmSession == drmSession) {
|
if (session == sourceDrmSession || session == codecDrmSession) {
|
||||||
drmSessionManager.releaseSession(pendingDrmSession);
|
// We already had this session. The manager must be reference counting, so release it once
|
||||||
|
// to get the count attributed to this renderer back down to 1.
|
||||||
|
drmSessionManager.releaseSession(session);
|
||||||
}
|
}
|
||||||
|
setSourceDrmSession(session);
|
||||||
} else {
|
} else {
|
||||||
pendingDrmSession = null;
|
setSourceDrmSession(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1143,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
|
// We have an existing codec that we may need to reconfigure or re-initialize. If the existing
|
||||||
// codec instance is being kept then its operating rate may need to be updated.
|
// codec instance is being kept then its operating rate may need to be updated.
|
||||||
if (pendingDrmSession != drmSession) {
|
|
||||||
|
if ((sourceDrmSession == null && codecDrmSession != null)
|
||||||
|
|| (sourceDrmSession != null && codecDrmSession == null)
|
||||||
|
|| (sourceDrmSession != null && !codecInfo.secure)
|
||||||
|
|| (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) {
|
||||||
|
// We might need to switch between the clear and protected output paths, or we're using DRM
|
||||||
|
// prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM
|
||||||
|
// session.
|
||||||
drainAndReinitializeCodec();
|
drainAndReinitializeCodec();
|
||||||
} else {
|
return;
|
||||||
switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {
|
}
|
||||||
case KEEP_CODEC_RESULT_NO:
|
|
||||||
drainAndReinitializeCodec();
|
switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {
|
||||||
break;
|
case KEEP_CODEC_RESULT_NO:
|
||||||
case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
|
drainAndReinitializeCodec();
|
||||||
|
break;
|
||||||
|
case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
|
||||||
|
codecFormat = newFormat;
|
||||||
|
updateCodecOperatingRate();
|
||||||
|
if (sourceDrmSession != codecDrmSession) {
|
||||||
|
drainAndUpdateCodecDrmSession();
|
||||||
|
} else {
|
||||||
drainAndFlushCodec();
|
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;
|
codecFormat = newFormat;
|
||||||
updateCodecOperatingRate();
|
updateCodecOperatingRate();
|
||||||
break;
|
if (sourceDrmSession != codecDrmSession) {
|
||||||
case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION:
|
drainAndUpdateCodecDrmSession();
|
||||||
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_WITHOUT_RECONFIGURATION:
|
break;
|
||||||
codecFormat = newFormat;
|
case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:
|
||||||
updateCodecOperatingRate();
|
codecFormat = newFormat;
|
||||||
break;
|
updateCodecOperatingRate();
|
||||||
default:
|
if (sourceDrmSession != codecDrmSession) {
|
||||||
throw new IllegalStateException(); // Never happens.
|
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
|
* Starts draining the codec for re-initialization. Re-initialization may occur immediately if no
|
||||||
* buffers have been queued to the codec.
|
* buffers have been queued to the codec.
|
||||||
@ -1323,8 +1392,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
codecDrainAction = DRAIN_ACTION_REINITIALIZE;
|
codecDrainAction = DRAIN_ACTION_REINITIALIZE;
|
||||||
} else {
|
} else {
|
||||||
// Nothing has been queued to the decoder, so we can re-initialize immediately.
|
// Nothing has been queued to the decoder, so we can re-initialize immediately.
|
||||||
releaseCodec();
|
reinitializeCodec();
|
||||||
maybeInitCodec();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1528,11 +1596,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
private void processEndOfStream() throws ExoPlaybackException {
|
private void processEndOfStream() throws ExoPlaybackException {
|
||||||
switch (codecDrainAction) {
|
switch (codecDrainAction) {
|
||||||
case DRAIN_ACTION_REINITIALIZE:
|
case DRAIN_ACTION_REINITIALIZE:
|
||||||
releaseCodec();
|
reinitializeCodec();
|
||||||
maybeInitCodec();
|
break;
|
||||||
|
case DRAIN_ACTION_UPDATE_DRM_SESSION:
|
||||||
|
updateDrmSessionOrReinitializeCodecV23();
|
||||||
break;
|
break;
|
||||||
case DRAIN_ACTION_FLUSH:
|
case DRAIN_ACTION_FLUSH:
|
||||||
flushOrReinitCodec();
|
flushOrReinitializeCodec();
|
||||||
break;
|
break;
|
||||||
case DRAIN_ACTION_NONE:
|
case DRAIN_ACTION_NONE:
|
||||||
default:
|
default:
|
||||||
@ -1542,6 +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) {
|
private boolean shouldSkipOutputBuffer(long presentationTimeUs) {
|
||||||
// We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would
|
// We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would
|
||||||
// box presentationTimeUs, creating a Long object that would need to be garbage collected.
|
// box presentationTimeUs, creating a Long object that would need to be garbage collected.
|
||||||
@ -1693,7 +1798,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
*/
|
*/
|
||||||
private static boolean codecNeedsEosFlushWorkaround(String name) {
|
private static boolean codecNeedsEosFlushWorkaround(String name) {
|
||||||
return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(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".equals(name)
|
||||||
|| "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
|
|| "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
|
||||||
}
|
}
|
||||||
|
@ -318,7 +318,23 @@ public final class MediaCodecUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Work around https://github.com/google/ExoPlayer/issues/4519.
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,8 +18,10 @@ package com.google.android.exoplayer2.metadata;
|
|||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A collection of metadata entries.
|
* A collection of metadata entries.
|
||||||
@ -76,6 +78,18 @@ public final class Metadata implements Parcelable {
|
|||||||
return entries[index];
|
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
|
@Override
|
||||||
public boolean equals(@Nullable Object obj) {
|
public boolean equals(@Nullable Object obj) {
|
||||||
if (this == obj) {
|
if (this == obj) {
|
||||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata;
|
|||||||
|
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
|
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.id3.Id3Decoder;
|
||||||
import com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder;
|
import com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
@ -46,38 +47,43 @@ public interface MetadataDecoderFactory {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Default {@link MetadataDecoder} implementation.
|
* Default {@link MetadataDecoder} implementation.
|
||||||
* <p>
|
*
|
||||||
* The formats supported by this factory are:
|
* <p>The formats supported by this factory are:
|
||||||
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>ID3 ({@link Id3Decoder})</li>
|
* <li>ID3 ({@link Id3Decoder})
|
||||||
* <li>EMSG ({@link EventMessageDecoder})</li>
|
* <li>EMSG ({@link EventMessageDecoder})
|
||||||
* <li>SCTE-35 ({@link SpliceInfoDecoder})</li>
|
* <li>SCTE-35 ({@link SpliceInfoDecoder})
|
||||||
|
* <li>ICY ({@link IcyDecoder})
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
MetadataDecoderFactory DEFAULT = new MetadataDecoderFactory() {
|
MetadataDecoderFactory DEFAULT =
|
||||||
|
new MetadataDecoderFactory() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsFormat(Format format) {
|
public boolean supportsFormat(Format format) {
|
||||||
String mimeType = format.sampleMimeType;
|
String mimeType = format.sampleMimeType;
|
||||||
return MimeTypes.APPLICATION_ID3.equals(mimeType)
|
return MimeTypes.APPLICATION_ID3.equals(mimeType)
|
||||||
|| MimeTypes.APPLICATION_EMSG.equals(mimeType)
|
|| MimeTypes.APPLICATION_EMSG.equals(mimeType)
|
||||||
|| MimeTypes.APPLICATION_SCTE35.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();
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
@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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -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
Loading…
x
Reference in New Issue
Block a user