Commit 20aeb0c8 by aujohn Committed by GitHub

Merge pull request #3 from google/dev-v2

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