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