offlineStreamKeys = getOfflineStreamKeys(uri);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(dataSourceFactory)
- .setManifestParser(
- new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
+ .setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory)
- .setManifestParser(
- new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
+ .setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory)
- .setPlaylistParserFactory(
- new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri)))
+ .setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
@@ -534,6 +523,9 @@ public class PlayerActivity extends Activity
mediaSource = null;
trackSelector = null;
}
+ if (adsLoader != null) {
+ adsLoader.setPlayer(null);
+ }
releaseMediaDrm();
}
@@ -597,6 +589,7 @@ public class PlayerActivity extends Activity
// The demo app has a non-null overlay frame layout.
playerView.getOverlayFrameLayout().addView(adUiViewGroup);
}
+ adsLoader.setPlayer(player);
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
new AdsMediaSource.MediaSourceFactory() {
@Override
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
index 20e27d8d48..5db52fd575 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
@@ -37,6 +37,7 @@ import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
@@ -177,7 +178,11 @@ public class SampleChooserActivity extends Activity
.show();
} else {
UriSample uriSample = (UriSample) sample;
- downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension);
+ RenderersFactory renderersFactory =
+ ((DemoApplication) getApplication())
+ .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
+ downloadTracker.toggleDownload(
+ this, sample.name, uriSample.uri, uriSample.extension, renderersFactory);
}
}
diff --git a/demos/main/src/main/res/drawable-hdpi/ic_edit.png b/demos/main/src/main/res/drawable-hdpi/ic_edit.png
new file mode 100755
index 0000000000..25678d6de9
Binary files /dev/null and b/demos/main/src/main/res/drawable-hdpi/ic_edit.png differ
diff --git a/demos/main/src/main/res/drawable-mdpi/ic_edit.png b/demos/main/src/main/res/drawable-mdpi/ic_edit.png
new file mode 100755
index 0000000000..dffcd9f61a
Binary files /dev/null and b/demos/main/src/main/res/drawable-mdpi/ic_edit.png differ
diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_edit.png b/demos/main/src/main/res/drawable-xhdpi/ic_edit.png
new file mode 100755
index 0000000000..82f8563d1e
Binary files /dev/null and b/demos/main/src/main/res/drawable-xhdpi/ic_edit.png differ
diff --git a/demos/main/src/main/res/drawable-xxhdpi/ic_edit.png b/demos/main/src/main/res/drawable-xxhdpi/ic_edit.png
new file mode 100755
index 0000000000..f00b4b68c5
Binary files /dev/null and b/demos/main/src/main/res/drawable-xxhdpi/ic_edit.png differ
diff --git a/demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png b/demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png
new file mode 100755
index 0000000000..a9f99417fb
Binary files /dev/null and b/demos/main/src/main/res/drawable-xxxhdpi/ic_edit.png differ
diff --git a/demos/main/src/main/res/layout/download_track_item.xml b/demos/main/src/main/res/layout/download_track_item.xml
new file mode 100644
index 0000000000..fe1c62b391
--- /dev/null
+++ b/demos/main/src/main/res/layout/download_track_item.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/main/src/main/res/layout/start_download_dialog.xml b/demos/main/src/main/res/layout/start_download_dialog.xml
index acb9af5d97..c182047ff8 100644
--- a/demos/main/src/main/res/layout/start_download_dialog.xml
+++ b/demos/main/src/main/res/layout/start_download_dialog.xml
@@ -13,7 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml
index 40f065b18e..7ac5a65a49 100644
--- a/demos/main/src/main/res/values/strings.xml
+++ b/demos/main/src/main/res/values/strings.xml
@@ -51,6 +51,10 @@
Playing sample without ads, as the IMA extension was not loaded
+ Edit selection
+
+ Preparing download…
+
Failed to start download
This demo app does not support downloading playlists
diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle
index f6821d5cd2..0baa074d4a 100644
--- a/extensions/cast/build.gradle
+++ b/extensions/cast/build.gradle
@@ -31,7 +31,7 @@ android {
}
dependencies {
- api 'com.google.android.gms:play-services-cast-framework:16.0.3'
+ api 'com.google.android.gms:play-services-cast-framework:16.1.2'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
implementation project(modulePrefix + 'library-core')
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
index 584ac68305..871c28b785 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
@@ -266,20 +266,29 @@ public final class CastPlayer extends BasePlayer {
// Player implementation.
@Override
+ @Nullable
public AudioComponent getAudioComponent() {
return null;
}
@Override
+ @Nullable
public VideoComponent getVideoComponent() {
return null;
}
@Override
+ @Nullable
public TextComponent getTextComponent() {
return null;
}
+ @Override
+ @Nullable
+ public MetadataComponent getMetadataComponent() {
+ return null;
+ }
+
@Override
public Looper getApplicationLooper() {
return Looper.getMainLooper();
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
index ab10f41d8f..88276c17fe 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
@@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
@@ -493,6 +494,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
throw new IOException("HTTP request with non-empty body must set Content-Type");
}
+ if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
+ requestBuilder.addHeader(
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
+ }
// Set the Range header.
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
StringBuilder rangeValue = new StringBuilder();
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
index 6f3c623f3f..c5b76002fa 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
@@ -37,6 +37,10 @@ import java.util.List;
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
+ // Error codes matching ffmpeg_jni.cc.
+ private static final int DECODER_ERROR_INVALID_DATA = -1;
+ private static final int DECODER_ERROR_OTHER = -2;
+
private final String codecName;
private final @Nullable byte[] extraData;
private final @C.Encoding int encoding;
@@ -106,8 +110,14 @@ import java.util.List;
int inputSize = inputData.limit();
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
- if (result < 0) {
- return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result);
+ if (result == DECODER_ERROR_INVALID_DATA) {
+ // Treat invalid data errors as non-fatal to match the behavior of MediaCodec. No output will
+ // be produced for this buffer, so mark it as decode-only to ensure that the audio sink's
+ // position is reset when more audio is produced.
+ outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
+ return null;
+ } else if (result == DECODER_ERROR_OTHER) {
+ return new FfmpegDecoderException("Error decoding (see logcat).");
}
if (!hasOutputFormat) {
channelCount = ffmpegGetChannelCount(nativeContext);
diff --git a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc
index 87579ebb9a..dcd4560e4a 100644
--- a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc
+++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc
@@ -63,6 +63,10 @@ static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16;
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
+// Error codes matching FfmpegDecoder.java.
+static const int DECODER_ERROR_INVALID_DATA = -1;
+static const int DECODER_ERROR_OTHER = -2;
+
/**
* Returns the AVCodec with the specified name, or NULL if it is not available.
*/
@@ -79,7 +83,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
/**
* Decodes the packet into the output buffer, returning the number of bytes
- * written, or a negative value in the case of an error.
+ * written, or a negative DECODER_ERROR constant value in the case of an error.
*/
int decodePacket(AVCodecContext *context, AVPacket *packet,
uint8_t *outputBuffer, int outputSize);
@@ -238,6 +242,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
context->channels = rawChannelCount;
context->channel_layout = av_get_default_channel_layout(rawChannelCount);
}
+ context->err_recognition = AV_EF_IGNORE_ERR;
int result = avcodec_open2(context, codec, NULL);
if (result < 0) {
logError("avcodec_open2", result);
@@ -254,7 +259,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
result = avcodec_send_packet(context, packet);
if (result) {
logError("avcodec_send_packet", result);
- return result;
+ return result == AVERROR_INVALIDDATA ? DECODER_ERROR_INVALID_DATA
+ : DECODER_ERROR_OTHER;
}
// Dequeue output data until it runs out.
diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle
index c845cb3423..6c0ec05bfb 100644
--- a/extensions/gvr/build.gradle
+++ b/extensions/gvr/build.gradle
@@ -33,9 +33,7 @@ dependencies {
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
- implementation 'com.google.vr:sdk-audio:1.80.0'
- implementation 'com.google.vr:sdk-controller:1.80.0'
- api 'com.google.vr:sdk-base:1.80.0'
+ api 'com.google.vr:sdk-base:1.190.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
index 40950bceef..311752c7ab 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
@@ -47,7 +47,6 @@ import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
@@ -74,7 +73,13 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
-/** Loads ads using the IMA SDK. All methods are called on the main thread. */
+/**
+ * {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread.
+ *
+ * The player instance that will play the loaded ads must be set before playback using {@link
+ * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
+ * {@link #release()}.
+ */
public final class ImaAdsLoader
implements Player.EventListener,
AdsLoader,
@@ -93,9 +98,9 @@ public final class ImaAdsLoader
private final Context context;
- private @Nullable ImaSdkSettings imaSdkSettings;
- private @Nullable AdEventListener adEventListener;
- private @Nullable Set adUiElements;
+ @Nullable private ImaSdkSettings imaSdkSettings;
+ @Nullable private AdEventListener adEventListener;
+ @Nullable private Set adUiElements;
private int vastLoadTimeoutMs;
private int mediaLoadTimeoutMs;
private int mediaBitrate;
@@ -317,10 +322,11 @@ public final class ImaAdsLoader
private final AdDisplayContainer adDisplayContainer;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
+ @Nullable private Player nextPlayer;
private Object pendingAdRequestContext;
private List supportedMimeTypes;
- private EventListener eventListener;
- private Player player;
+ @Nullable private EventListener eventListener;
+ @Nullable private Player player;
private VideoProgressUpdate lastContentProgress;
private VideoProgressUpdate lastAdProgress;
private int lastVolumePercentage;
@@ -526,6 +532,14 @@ public final class ImaAdsLoader
// AdsLoader implementation.
+ @Override
+ public void setPlayer(@Nullable Player player) {
+ Assertions.checkState(Looper.getMainLooper() == Looper.myLooper());
+ Assertions.checkState(
+ player == null || player.getApplicationLooper() == Looper.getMainLooper());
+ nextPlayer = player;
+ }
+
@Override
public void setSupportedContentTypes(@C.ContentType int... contentTypes) {
List supportedMimeTypes = new ArrayList<>();
@@ -550,9 +564,10 @@ public final class ImaAdsLoader
}
@Override
- public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
- Assertions.checkArgument(player.getApplicationLooper() == Looper.getMainLooper());
- this.player = player;
+ public void start(EventListener eventListener, ViewGroup adUiViewGroup) {
+ Assertions.checkNotNull(
+ nextPlayer, "Set player using adsLoader.setPlayer before preparing the player.");
+ player = nextPlayer;
this.eventListener = eventListener;
lastVolumePercentage = 0;
lastAdProgress = null;
@@ -576,7 +591,7 @@ public final class ImaAdsLoader
}
@Override
- public void detachPlayer() {
+ public void stop() {
if (adsManager != null && imaPausedContent) {
adPlaybackState =
adPlaybackState.withAdResumePositionUs(
@@ -598,6 +613,8 @@ public final class ImaAdsLoader
adsManager.destroy();
adsManager = null;
}
+ adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this);
+ adsLoader.removeAdErrorListener(/* adErrorListener= */ this);
imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE;
pendingAdLoadError = null;
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
index 85042c4354..bcccd6cec7 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.ima;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
-import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
@@ -33,7 +32,8 @@ import java.io.IOException;
/**
* A {@link MediaSource} that inserts ads linearly with a provided content media source.
*
- * @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
+ * @deprecated Use {@link com.google.android.exoplayer2.source.ads.AdsMediaSource} with
+ * ImaAdsLoader.
*/
@Deprecated
public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener {
@@ -83,12 +83,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
}
@Override
- public void prepareSourceInternal(
- final ExoPlayer player,
- boolean isTopLevelSource,
- @Nullable TransferListener mediaTransferListener) {
- adsMediaSource.prepareSource(
- player, isTopLevelSource, /* listener= */ this, mediaTransferListener);
+ public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ adsMediaSource.prepareSource(/* listener= */ this, mediaTransferListener);
}
@Override
@@ -97,8 +93,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
}
@Override
- public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
- return adsMediaSource.createPeriod(id, allocator);
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ return adsMediaSource.createPeriod(id, allocator, startPositionUs);
}
@Override
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java
index b626a08780..59dfc6473c 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java
@@ -64,14 +64,17 @@ import java.util.Set;
};
}
+ @Override
public int getVastMediaWidth() {
throw new UnsupportedOperationException();
}
+ @Override
public int getVastMediaHeight() {
throw new UnsupportedOperationException();
}
+ @Override
public int getVastMediaBitrate() {
throw new UnsupportedOperationException();
}
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
index b0fe731480..0b097f26f0 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
@@ -111,7 +111,7 @@ public class ImaAdsLoaderTest {
@Test
public void testAttachPlayer_setsAdUiViewGroup() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
- imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
verify(adDisplayContainer, atLeastOnce()).setAdContainer(adUiViewGroup);
}
@@ -119,7 +119,7 @@ public class ImaAdsLoaderTest {
@Test
public void testAttachPlayer_updatesAdPlaybackState() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
- imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
@@ -131,14 +131,14 @@ public class ImaAdsLoaderTest {
public void testAttachAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
- imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
}
@Test
public void testAttachAndCallbacksAfterRelease() {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
- imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
fakeExoPlayer.setState(Player.STATE_READY, true);
@@ -166,7 +166,7 @@ public class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
// Load the preroll ad.
- imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup);
+ imaAdsLoader.start(adsLoaderListener, adUiViewGroup);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD));
imaAdsLoader.loadAd(TEST_URI.toString());
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD));
@@ -210,6 +210,7 @@ public class ImaAdsLoaderTest {
.setImaFactory(testImaFactory)
.setImaSdkSettings(imaSdkSettings)
.buildForAdTag(TEST_URI);
+ imaAdsLoader.setPlayer(fakeExoPlayer);
}
private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) {
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
index b7818546f9..677d3c2ebd 100644
--- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
+++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
@@ -129,7 +129,7 @@ public final class JobDispatcherScheduler implements Scheduler {
Bundle extras = new Bundle();
extras.putString(KEY_SERVICE_ACTION, serviceAction);
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
- extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
+ extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
builder.setExtras(extras);
return builder.build();
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
index de14cbf6d7..b4811f040a 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -67,10 +67,10 @@ import java.util.Map;
*
*
* - Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and {@code
- * PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed
- * when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom
- * actions can be handled by passing one or more {@link CustomActionProvider}s in a similar
- * way.
+ * PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed to
+ * {@link #setPlaybackPreparer(PlaybackPreparer)}.
+ *
- Custom actions can be handled by passing one or more {@link CustomActionProvider}s to
+ * {@link #setCustomActionProviders(CustomActionProvider...)}.
*
- To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by
* calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator}
* is recommended for most use cases.
@@ -339,21 +339,21 @@ public final class MediaSessionConnector {
/** The wrapped {@link MediaSessionCompat}. */
public final MediaSessionCompat mediaSession;
- @Nullable private final MediaMetadataProvider mediaMetadataProvider;
- private final ExoPlayerEventListener exoPlayerEventListener;
- private final MediaSessionCallback mediaSessionCallback;
+ private final Looper looper;
+ private final ComponentListener componentListener;
private final ArrayList commandReceivers;
- private Player player;
private ControlDispatcher controlDispatcher;
private CustomActionProvider[] customActionProviders;
private Map customActionMap;
+ @Nullable private MediaMetadataProvider mediaMetadataProvider;
+ @Nullable private Player player;
@Nullable private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
@Nullable private Pair customError;
- private PlaybackPreparer playbackPreparer;
- private QueueNavigator queueNavigator;
- private QueueEditor queueEditor;
- private RatingCallback ratingCallback;
+ @Nullable private PlaybackPreparer playbackPreparer;
+ @Nullable private QueueNavigator queueNavigator;
+ @Nullable private QueueEditor queueEditor;
+ @Nullable private RatingCallback ratingCallback;
private long enabledPlaybackActions;
private int rewindMs;
@@ -362,82 +362,60 @@ public final class MediaSessionConnector {
/**
* Creates an instance.
*
- *
Equivalent to {@code MediaSessionConnector(mediaSession, new
- * DefaultMediaMetadataProvider(mediaSession.getController(), null))}.
- *
* @param mediaSession The {@link MediaSessionCompat} to connect to.
*/
public MediaSessionConnector(MediaSessionCompat mediaSession) {
- this(
- mediaSession,
- new DefaultMediaMetadataProvider(mediaSession.getController(), null));
- }
-
- /**
- * Creates an instance.
- *
- * @param mediaSession The {@link MediaSessionCompat} to connect to.
- * @param mediaMetadataProvider A {@link MediaMetadataProvider} for providing a custom metadata
- * object to be published to the media session, or {@code null} if metadata shouldn't be
- * published.
- */
- public MediaSessionConnector(
- MediaSessionCompat mediaSession,
- @Nullable MediaMetadataProvider mediaMetadataProvider) {
this.mediaSession = mediaSession;
- this.mediaMetadataProvider = mediaMetadataProvider;
- mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
- mediaSessionCallback = new MediaSessionCallback();
- exoPlayerEventListener = new ExoPlayerEventListener();
- controlDispatcher = new DefaultControlDispatcher();
- customActionMap = Collections.emptyMap();
+ looper = Util.getLooper();
+ componentListener = new ComponentListener();
commandReceivers = new ArrayList<>();
+ controlDispatcher = new DefaultControlDispatcher();
+ customActionProviders = new CustomActionProvider[0];
+ customActionMap = Collections.emptyMap();
+ mediaMetadataProvider =
+ new DefaultMediaMetadataProvider(
+ mediaSession.getController(), /* metadataExtrasPrefix= */ null);
enabledPlaybackActions = DEFAULT_PLAYBACK_ACTIONS;
rewindMs = DEFAULT_REWIND_MS;
fastForwardMs = DEFAULT_FAST_FORWARD_MS;
+ mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS);
+ mediaSession.setCallback(componentListener, new Handler(looper));
}
/**
* Sets the player to be connected to the media session. Must be called on the same thread that is
* used to access the player.
*
- *
The order in which any {@link CustomActionProvider}s are passed determines the order of the
- * actions published with the playback state of the session.
- *
* @param player The player to be connected to the {@code MediaSession}, or {@code null} to
* disconnect the current player.
- * @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player.
- * @param customActionProviders Optional {@link CustomActionProvider}s to publish and handle
- * custom actions.
*/
- public void setPlayer(
- @Nullable Player player,
- @Nullable PlaybackPreparer playbackPreparer,
- CustomActionProvider... customActionProviders) {
- Assertions.checkArgument(player == null || player.getApplicationLooper() == Looper.myLooper());
+ public void setPlayer(@Nullable Player player) {
+ Assertions.checkArgument(player == null || player.getApplicationLooper() == looper);
if (this.player != null) {
- this.player.removeListener(exoPlayerEventListener);
- mediaSession.setCallback(null);
+ this.player.removeListener(componentListener);
}
-
- unregisterCommandReceiver(this.playbackPreparer);
this.player = player;
- this.playbackPreparer = playbackPreparer;
- registerCommandReceiver(playbackPreparer);
-
- this.customActionProviders =
- (player != null && customActionProviders != null)
- ? customActionProviders
- : new CustomActionProvider[0];
if (player != null) {
- Handler handler = new Handler(Util.getLooper());
- mediaSession.setCallback(mediaSessionCallback, handler);
- player.addListener(exoPlayerEventListener);
+ player.addListener(componentListener);
}
invalidateMediaSessionPlaybackState();
invalidateMediaSessionMetadata();
}
+ /**
+ * Sets the {@link PlaybackPreparer}.
+ *
+ * @param playbackPreparer The {@link PlaybackPreparer}.
+ */
+ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
+ if (this.playbackPreparer != playbackPreparer) {
+ unregisterCommandReceiver(this.playbackPreparer);
+ this.playbackPreparer = playbackPreparer;
+ registerCommandReceiver(playbackPreparer);
+ invalidateMediaSessionPlaybackState();
+ }
+ }
+
/**
* Sets the {@link ControlDispatcher}.
*
@@ -570,6 +548,32 @@ public final class MediaSessionConnector {
invalidateMediaSessionPlaybackState();
}
+ /**
+ * Sets custom action providers. The order of the {@link CustomActionProvider}s determines the
+ * order in which the actions are published.
+ *
+ * @param customActionProviders The custom action providers, or null to remove all existing custom
+ * action providers.
+ */
+ public void setCustomActionProviders(@Nullable CustomActionProvider... customActionProviders) {
+ this.customActionProviders =
+ customActionProviders == null ? new CustomActionProvider[0] : customActionProviders;
+ invalidateMediaSessionPlaybackState();
+ }
+
+ /**
+ * Sets a provider of metadata to be published to the media session.
+ *
+ * @param mediaMetadataProvider The provider of metadata to publish, or {@code null} if no
+ * metadata should be published.
+ */
+ public void setMediaMetadataProvider(@Nullable MediaMetadataProvider mediaMetadataProvider) {
+ if (this.mediaMetadataProvider != mediaMetadataProvider) {
+ this.mediaMetadataProvider = mediaMetadataProvider;
+ invalidateMediaSessionMetadata();
+ }
+ }
+
/**
* Updates the metadata of the media session.
*
@@ -577,9 +581,11 @@ public final class MediaSessionConnector {
* changed and the metadata should be updated immediately.
*/
public final void invalidateMediaSessionMetadata() {
- if (mediaMetadataProvider != null && player != null) {
- mediaSession.setMetadata(mediaMetadataProvider.getMetadata(player));
- }
+ MediaMetadataCompat metadata =
+ mediaMetadataProvider != null && player != null
+ ? mediaMetadataProvider.getMetadata(player)
+ : null;
+ mediaSession.setMetadata(metadata);
}
/**
@@ -591,7 +597,7 @@ public final class MediaSessionConnector {
public final void invalidateMediaSessionPlaybackState() {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
if (player == null) {
- builder.setActions(/* capabilities= */ 0).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
+ builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
mediaSession.setPlaybackState(builder.build());
return;
}
@@ -627,7 +633,7 @@ public final class MediaSessionConnector {
Bundle extras = new Bundle();
extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch);
builder
- .setActions(buildPlaybackActions(player))
+ .setActions(buildPrepareActions() | buildPlaybackActions(player))
.setActiveQueueItemId(activeQueueItemId)
.setBufferedPosition(player.getBufferedPosition())
.setState(
@@ -662,6 +668,12 @@ public final class MediaSessionConnector {
commandReceivers.remove(commandReceiver);
}
+ private long buildPrepareActions() {
+ return playbackPreparer == null
+ ? 0
+ : (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
+ }
+
private long buildPlaybackActions(Player player) {
boolean enableSeeking = false;
boolean enableRewind = false;
@@ -688,9 +700,6 @@ public final class MediaSessionConnector {
playbackActions &= enabledPlaybackActions;
long actions = playbackActions;
- if (playbackPreparer != null) {
- actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions());
- }
if (queueNavigator != null) {
actions |=
(QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions(player));
@@ -719,8 +728,7 @@ public final class MediaSessionConnector {
}
private boolean canDispatchToPlaybackPreparer(long action) {
- return player != null
- && playbackPreparer != null
+ return playbackPreparer != null
&& (playbackPreparer.getSupportedPrepareActions() & action) != 0;
}
@@ -738,6 +746,13 @@ public final class MediaSessionConnector {
return player != null && queueEditor != null;
}
+ private void stopPlayerForPrepare(boolean playWhenReady) {
+ if (player != null) {
+ player.stop();
+ player.setPlayWhenReady(playWhenReady);
+ }
+ }
+
private void rewind(Player player) {
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
seekTo(player, player.getCurrentPosition() - rewindMs);
@@ -865,11 +880,14 @@ public final class MediaSessionConnector {
}
}
- private class ExoPlayerEventListener implements Player.EventListener {
+ private class ComponentListener extends MediaSessionCompat.Callback
+ implements Player.EventListener {
private int currentWindowIndex;
private int currentWindowCount;
+ // Player.EventListener implementation.
+
@Override
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
@@ -932,9 +950,8 @@ public final class MediaSessionConnector {
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
invalidateMediaSessionPlaybackState();
}
- }
- private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ // MediaSessionCompat.Callback implementation.
@Override
public void onPlay() {
@@ -1058,8 +1075,7 @@ public final class MediaSessionConnector {
@Override
public void onPrepare() {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
- player.stop();
- player.setPlayWhenReady(false);
+ stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepare();
}
}
@@ -1067,8 +1083,7 @@ public final class MediaSessionConnector {
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
- player.stop();
- player.setPlayWhenReady(false);
+ stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
}
}
@@ -1076,8 +1091,7 @@ public final class MediaSessionConnector {
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
- player.stop();
- player.setPlayWhenReady(false);
+ stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromSearch(query, extras);
}
}
@@ -1085,8 +1099,7 @@ public final class MediaSessionConnector {
@Override
public void onPrepareFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
- player.stop();
- player.setPlayWhenReady(false);
+ stopPlayerForPrepare(/* playWhenReady= */ false);
playbackPreparer.onPrepareFromUri(uri, extras);
}
}
@@ -1094,8 +1107,7 @@ public final class MediaSessionConnector {
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
- player.stop();
- player.setPlayWhenReady(true);
+ stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
}
}
@@ -1103,8 +1115,7 @@ public final class MediaSessionConnector {
@Override
public void onPlayFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
- player.stop();
- player.setPlayWhenReady(true);
+ stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromSearch(query, extras);
}
}
@@ -1112,8 +1123,7 @@ public final class MediaSessionConnector {
@Override
public void onPlayFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
- player.stop();
- player.setPlayWhenReady(true);
+ stopPlayerForPrepare(/* playWhenReady= */ true);
playbackPreparer.onPrepareFromUri(uri, extras);
}
}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
index b773396198..617b8781f4 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java
@@ -22,9 +22,7 @@ import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.util.RepeatModeUtil;
-/**
- * Provides a custom action for toggling repeat modes.
- */
+/** Provides a custom action for toggling repeat modes. */
public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
/** The default repeat toggle modes. */
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
index 093913fd8c..b92d7a27b7 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java
@@ -65,13 +65,6 @@ public final class TimelineQueueEditor
* {@link MediaSessionConnector}.
*/
public interface QueueDataAdapter {
- /**
- * Gets the {@link MediaDescriptionCompat} for a {@code position}.
- *
- * @param position The position in the queue for which to provide a description.
- * @return A {@link MediaDescriptionCompat}.
- */
- MediaDescriptionCompat getMediaDescription(int position);
/**
* Adds a {@link MediaDescriptionCompat} at the given {@code position}.
*
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
index 5d2b37618f..d0047637dd 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java
@@ -41,7 +41,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
private final MediaSessionCompat mediaSession;
private final Timeline.Window window;
- protected final int maxQueueSize;
+ private final int maxQueueSize;
private long activeQueueItemId;
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
index 778277fdbc..dd1db8211a 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java
@@ -21,6 +21,7 @@ import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
@@ -263,7 +264,6 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
long position = dataSpec.position;
long length = dataSpec.length;
- boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
if (url == null) {
@@ -293,10 +293,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
if (userAgent != null) {
builder.addHeader("User-Agent", userAgent);
}
-
- if (!allowGzip) {
+ if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
builder.addHeader("Accept-Encoding", "identity");
}
+ if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
+ builder.addHeader(
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
+ IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
+ }
RequestBody requestBody = null;
if (dataSpec.httpBody != null) {
requestBody = RequestBody.create(null, dataSpec.httpBody);
diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md
index b222bdabd9..3863dff965 100644
--- a/extensions/rtmp/README.md
+++ b/extensions/rtmp/README.md
@@ -39,7 +39,7 @@ either instantiated and injected from application code, or obtained from
instances of `DataSource.Factory` that are instantiated and injected from
application code.
-`DefaultDataSource` will automatically use uses the RTMP extension whenever it's
+`DefaultDataSource` will automatically use the RTMP extension whenever it's
available. Hence if your application is using `DefaultDataSource` or
`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as
adding a dependency to the RTMP extension as described above. No changes to your
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
index e3081cd2d2..e61030a2e1 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
@@ -127,8 +127,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private VpxDecoder decoder;
private VpxInputBuffer inputBuffer;
private VpxOutputBuffer outputBuffer;
- private DrmSession drmSession;
- private DrmSession pendingDrmSession;
+ @Nullable private DrmSession decoderDrmSession;
+ @Nullable private DrmSession sourceDrmSession;
private @ReinitializationState int decoderReinitializationState;
private boolean decoderReceivedBuffers;
@@ -364,24 +364,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
clearReportedVideoSize();
clearRenderedFirstFrame();
try {
+ setSourceDrmSession(null);
releaseDecoder();
} finally {
- try {
- if (drmSession != null) {
- drmSessionManager.releaseSession(drmSession);
- }
- } finally {
- try {
- if (pendingDrmSession != null && pendingDrmSession != drmSession) {
- drmSessionManager.releaseSession(pendingDrmSession);
- }
- } finally {
- drmSession = null;
- pendingDrmSession = null;
- decoderCounters.ensureUpdated();
- eventDispatcher.disabled(decoderCounters);
- }
- }
+ eventDispatcher.disabled(decoderCounters);
}
}
@@ -433,18 +419,35 @@ public class LibvpxVideoRenderer extends BaseRenderer {
/** Releases the decoder. */
@CallSuper
protected void releaseDecoder() {
- if (decoder == null) {
- return;
- }
-
inputBuffer = null;
outputBuffer = null;
- decoder.release();
- decoder = null;
- decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false;
buffersInCodecCount = 0;
+ if (decoder != null) {
+ decoder.release();
+ decoder = null;
+ decoderCounters.decoderReleaseCount++;
+ }
+ setDecoderDrmSession(null);
+ }
+
+ private void setSourceDrmSession(@Nullable DrmSession session) {
+ DrmSession previous = sourceDrmSession;
+ sourceDrmSession = session;
+ releaseDrmSessionIfUnused(previous);
+ }
+
+ private void setDecoderDrmSession(@Nullable DrmSession session) {
+ DrmSession previous = decoderDrmSession;
+ decoderDrmSession = session;
+ releaseDrmSessionIfUnused(previous);
+ }
+
+ private void releaseDrmSessionIfUnused(@Nullable DrmSession session) {
+ if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
+ drmSessionManager.releaseSession(session);
+ }
}
/**
@@ -467,16 +470,20 @@ public class LibvpxVideoRenderer extends BaseRenderer {
throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
}
- pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
- if (pendingDrmSession == drmSession) {
- drmSessionManager.releaseSession(pendingDrmSession);
+ DrmSession session =
+ drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
+ if (session == decoderDrmSession || session == sourceDrmSession) {
+ // We already had this session. The manager must be reference counting, so release it once
+ // to get the count attributed to this renderer back down to 1.
+ drmSessionManager.releaseSession(session);
}
+ setSourceDrmSession(session);
} else {
- pendingDrmSession = null;
+ setSourceDrmSession(null);
}
}
- if (pendingDrmSession != drmSession) {
+ if (sourceDrmSession != decoderDrmSession) {
if (decoderReceivedBuffers) {
// Signal end of stream and wait for any final output buffers before re-initialization.
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
@@ -704,12 +711,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
return;
}
- drmSession = pendingDrmSession;
+ setDecoderDrmSession(sourceDrmSession);
+
ExoMediaCrypto mediaCrypto = null;
- if (drmSession != null) {
- mediaCrypto = drmSession.getMediaCrypto();
+ if (decoderDrmSession != null) {
+ mediaCrypto = decoderDrmSession.getMediaCrypto();
if (mediaCrypto == null) {
- DrmSessionException drmError = drmSession.getError();
+ DrmSessionException drmError = decoderDrmSession.getError();
if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
// input format causes the session to be replaced before it's used.
@@ -922,12 +930,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
+ if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
- @DrmSession.State int drmSessionState = drmSession.getState();
+ @DrmSession.State int drmSessionState = decoderDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
}
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java
index fac9818d9e..8810b51000 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/C.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java
@@ -460,8 +460,8 @@ public final class C {
/**
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
- * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_ENCRYPTED} and
- * {@link #BUFFER_FLAG_DECODE_ONLY}.
+ * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE},
+ * {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@@ -470,6 +470,7 @@ public final class C {
value = {
BUFFER_FLAG_KEY_FRAME,
BUFFER_FLAG_END_OF_STREAM,
+ BUFFER_FLAG_LAST_SAMPLE,
BUFFER_FLAG_ENCRYPTED,
BUFFER_FLAG_DECODE_ONLY
})
@@ -482,6 +483,8 @@ public final class C {
* Flag for empty buffers that signal that the end of the stream was reached.
*/
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+ /** Indicates that a buffer is known to contain the last media sample of the stream. */
+ public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000
/** Indicates that a buffer is (at least partially) encrypted. */
public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
/** Indicates that a buffer should be decoded but not rendered. */
@@ -896,6 +899,26 @@ public final class C {
*/
public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
+ /** Video projection types. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ Format.NO_VALUE,
+ PROJECTION_RECTANGULAR,
+ PROJECTION_EQUIRECTANGULAR,
+ PROJECTION_CUBEMAP,
+ PROJECTION_MESH
+ })
+ public @interface Projection {}
+ /** Conventional rectangular projection. */
+ public static final int PROJECTION_RECTANGULAR = 0;
+ /** Equirectangular spherical projection. */
+ public static final int PROJECTION_EQUIRECTANGULAR = 1;
+ /** Cube map projection. */
+ public static final int PROJECTION_CUBEMAP = 2;
+ /** 3-D mesh projection. */
+ public static final int PROJECTION_MESH = 3;
+
/**
* Priority for media playback.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index a56c8e3b90..8736417362 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -139,26 +139,34 @@ import java.util.concurrent.CopyOnWriteArrayList;
repeatMode,
shuffleModeEnabled,
eventHandler,
- this,
clock);
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
}
@Override
+ @Nullable
public AudioComponent getAudioComponent() {
return null;
}
@Override
+ @Nullable
public VideoComponent getVideoComponent() {
return null;
}
@Override
+ @Nullable
public TextComponent getTextComponent() {
return null;
}
+ @Override
+ @Nullable
+ public MetadataComponent getMetadataComponent() {
+ return null;
+ }
+
@Override
public Looper getPlaybackLooper() {
return internalPlayer.getPlaybackLooper();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index 2a161b79bd..b4549362f3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -95,7 +95,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
private final HandlerWrapper handler;
private final HandlerThread internalPlaybackThread;
private final Handler eventHandler;
- private final ExoPlayer player;
private final Timeline.Window window;
private final Timeline.Period period;
private final long backBufferDurationUs;
@@ -134,7 +133,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled,
Handler eventHandler,
- ExoPlayer player,
Clock clock) {
this.renderers = renderers;
this.trackSelector = trackSelector;
@@ -145,7 +143,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
this.repeatMode = repeatMode;
this.shuffleModeEnabled = shuffleModeEnabled;
this.eventHandler = eventHandler;
- this.player = player;
this.clock = clock;
this.queue = new MediaPeriodQueue();
@@ -441,11 +438,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
loadControl.onPrepared();
this.mediaSource = mediaSource;
setState(Player.STATE_BUFFERING);
- mediaSource.prepareSource(
- player,
- /* isTopLevelSource= */ true,
- /* listener= */ this,
- bandwidthMeter.getTransferListener());
+ mediaSource.prepareSource(/* listener= */ this, bandwidthMeter.getTransferListener());
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
index c30fe160c9..36723c5d73 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
- public static final String VERSION = "2.9.2";
+ public static final String VERSION = "2.9.4";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.2";
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.4";
/**
* The version of the library expressed as an integer, for example 1002003.
@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final int VERSION_INT = 2009002;
+ public static final int VERSION_INT = 2009004;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
index 3456fc39a2..6c54c07cde 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
@@ -1181,6 +1181,37 @@ public final class Format implements Parcelable {
metadata);
}
+ public Format copyWithFrameRate(float frameRate) {
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ subsampleOffsetUs,
+ initializationData,
+ drmInitData,
+ metadata);
+ }
+
public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {
return new Format(
id,
@@ -1274,6 +1305,37 @@ public final class Format implements Parcelable {
metadata);
}
+ public Format copyWithBitrate(int bitrate) {
+ return new Format(
+ id,
+ label,
+ containerMimeType,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ maxInputSize,
+ width,
+ height,
+ frameRate,
+ rotationDegrees,
+ pixelWidthHeightRatio,
+ projectionData,
+ stereoMode,
+ colorInfo,
+ channelCount,
+ sampleRate,
+ pcmEncoding,
+ encoderDelay,
+ encoderPadding,
+ selectionFlags,
+ language,
+ accessibilityChannel,
+ subsampleOffsetUs,
+ initializationData,
+ drmInitData,
+ metadata);
+ }
+
/**
* Returns the number of pixels if this is a video format whose {@link #width} and {@link #height}
* are known, or {@link #NO_VALUE} otherwise
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
index 7becac7b55..19622c6801 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
@@ -89,7 +89,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
this.info = info;
sampleStreams = new SampleStream[rendererCapabilities.length];
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
- mediaPeriod = createMediaPeriod(info.id, mediaSource, allocator);
+ mediaPeriod = createMediaPeriod(info.id, mediaSource, allocator, info.startPositionUs);
}
/**
@@ -399,8 +399,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Returns a media period corresponding to the given {@code id}. */
private static MediaPeriod createMediaPeriod(
- MediaPeriodId id, MediaSource mediaSource, Allocator allocator) {
- MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator);
+ MediaPeriodId id, MediaSource mediaSource, Allocator allocator, long startPositionUs) {
+ MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs);
if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) {
mediaPeriod =
new ClippingMediaPeriod(
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
index 16f8aa2878..e3441fb2a7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.C.VideoScalingMode;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioListener;
import com.google.android.exoplayer2.audio.AuxEffectInfo;
+import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@@ -299,6 +300,24 @@ public interface Player {
void removeTextOutput(TextOutput listener);
}
+ /** The metadata component of a {@link Player}. */
+ interface MetadataComponent {
+
+ /**
+ * Adds a {@link MetadataOutput} to receive metadata.
+ *
+ * @param output The output to register.
+ */
+ void addMetadataOutput(MetadataOutput output);
+
+ /**
+ * Removes a {@link MetadataOutput}.
+ *
+ * @param output The output to remove.
+ */
+ void removeMetadataOutput(MetadataOutput output);
+ }
+
/**
* Listener of changes in player state. All methods have no-op default implementations to allow
* selective overrides.
@@ -533,6 +552,12 @@ public interface Player {
@Nullable
TextComponent getTextComponent();
+ /**
+ * Returns the component of this player for metadata output, or null if metadata is not supported.
+ */
+ @Nullable
+ MetadataComponent getMetadataComponent();
+
/**
* Returns the {@link Looper} associated with the application thread that's used to access the
* player and on which player events are received.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
index 4ca6b51ce2..e498038fde 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -65,7 +65,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
*/
@TargetApi(16)
public class SimpleExoPlayer extends BasePlayer
- implements ExoPlayer, Player.AudioComponent, Player.VideoComponent, Player.TextComponent {
+ implements ExoPlayer,
+ Player.AudioComponent,
+ Player.VideoComponent,
+ Player.TextComponent,
+ Player.MetadataComponent {
/** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */
@Deprecated
@@ -90,25 +94,25 @@ public class SimpleExoPlayer extends BasePlayer
private final AudioFocusManager audioFocusManager;
- private Format videoFormat;
- private Format audioFormat;
+ @Nullable private Format videoFormat;
+ @Nullable private Format audioFormat;
- private Surface surface;
+ @Nullable private Surface surface;
private boolean ownsSurface;
private @C.VideoScalingMode int videoScalingMode;
- private SurfaceHolder surfaceHolder;
- private TextureView textureView;
+ @Nullable private SurfaceHolder surfaceHolder;
+ @Nullable private TextureView textureView;
private int surfaceWidth;
private int surfaceHeight;
- private DecoderCounters videoDecoderCounters;
- private DecoderCounters audioDecoderCounters;
+ @Nullable private DecoderCounters videoDecoderCounters;
+ @Nullable private DecoderCounters audioDecoderCounters;
private int audioSessionId;
private AudioAttributes audioAttributes;
private float audioVolume;
- private MediaSource mediaSource;
+ @Nullable private MediaSource mediaSource;
private List currentCues;
- private VideoFrameMetadataListener videoFrameMetadataListener;
- private CameraMotionListener cameraMotionListener;
+ @Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
+ @Nullable private CameraMotionListener cameraMotionListener;
private boolean hasNotifiedFullWrongThreadWarning;
/**
@@ -243,20 +247,29 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
+ @Nullable
public AudioComponent getAudioComponent() {
return this;
}
@Override
+ @Nullable
public VideoComponent getVideoComponent() {
return this;
}
@Override
+ @Nullable
public TextComponent getTextComponent() {
return this;
}
+ @Override
+ @Nullable
+ public MetadataComponent getMetadataComponent() {
+ return this;
+ }
+
/**
* Sets the video scaling mode.
*
@@ -545,30 +558,26 @@ public class SimpleExoPlayer extends BasePlayer
setPlaybackParameters(playbackParameters);
}
- /**
- * Returns the video format currently being played, or null if no video is being played.
- */
+ /** Returns the video format currently being played, or null if no video is being played. */
+ @Nullable
public Format getVideoFormat() {
return videoFormat;
}
- /**
- * Returns the audio format currently being played, or null if no audio is being played.
- */
+ /** Returns the audio format currently being played, or null if no audio is being played. */
+ @Nullable
public Format getAudioFormat() {
return audioFormat;
}
- /**
- * Returns {@link DecoderCounters} for video, or null if no video is being played.
- */
+ /** Returns {@link DecoderCounters} for video, or null if no video is being played. */
+ @Nullable
public DecoderCounters getVideoDecoderCounters() {
return videoDecoderCounters;
}
- /**
- * Returns {@link DecoderCounters} for audio, or null if no audio is being played.
- */
+ /** Returns {@link DecoderCounters} for audio, or null if no audio is being played. */
+ @Nullable
public DecoderCounters getAudioDecoderCounters() {
return audioDecoderCounters;
}
@@ -713,20 +722,12 @@ public class SimpleExoPlayer extends BasePlayer
removeTextOutput(output);
}
- /**
- * Adds a {@link MetadataOutput} to receive metadata.
- *
- * @param listener The output to register.
- */
+ @Override
public void addMetadataOutput(MetadataOutput listener) {
metadataOutputs.add(listener);
}
- /**
- * Removes a {@link MetadataOutput}.
- *
- * @param listener The output to remove.
- */
+ @Override
public void removeMetadataOutput(MetadataOutput listener) {
metadataOutputs.remove(listener);
}
@@ -1048,7 +1049,8 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
- public @Nullable Object getCurrentManifest() {
+ @Nullable
+ public Object getCurrentManifest() {
verifyApplicationThread();
return player.getCurrentManifest();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
index 113add612a..55031e2d12 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
@@ -488,7 +488,10 @@ public class AnalyticsCollector
@Override
public final void onPlayerError(ExoPlaybackException error) {
- EventTime eventTime = generatePlayingMediaPeriodEventTime();
+ EventTime eventTime =
+ error.type == ExoPlaybackException.TYPE_SOURCE
+ ? generateLoadingMediaPeriodEventTime()
+ : generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlayerError(eventTime, error);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java
index eff7bc8de2..48fbea75b4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java
@@ -147,6 +147,7 @@ public interface AudioRendererEventListener {
* Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
*/
public void disabled(final DecoderCounters counters) {
+ counters.ensureUpdated();
if (listener != null) {
handler.post(
() -> {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
index 49c391c4cc..7fc6c16db8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -548,7 +548,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
try {
super.onDisabled();
} finally {
- decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
index 287cae9d41..f2e8a23811 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
@@ -106,8 +106,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
? extends AudioDecoderException> decoder;
private DecoderInputBuffer inputBuffer;
private SimpleOutputBuffer outputBuffer;
- private DrmSession drmSession;
- private DrmSession pendingDrmSession;
+ @Nullable private DrmSession decoderDrmSession;
+ @Nullable private DrmSession sourceDrmSession;
@ReinitializationState private int decoderReinitializationState;
private boolean decoderReceivedBuffers;
@@ -366,7 +366,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
if (outputBuffer == null) {
return false;
}
- decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
+ if (outputBuffer.skippedOutputBufferCount > 0) {
+ decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
+ audioSink.handleDiscontinuity();
+ }
}
if (outputBuffer.isEndOfStream()) {
@@ -459,12 +462,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
+ if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
- @DrmSession.State int drmSessionState = drmSession.getState();
+ @DrmSession.State int drmSessionState = decoderDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
}
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
@@ -565,25 +568,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
audioTrackNeedsConfigure = true;
waitingForKeys = false;
try {
+ setSourceDrmSession(null);
releaseDecoder();
audioSink.reset();
} finally {
- try {
- if (drmSession != null) {
- drmSessionManager.releaseSession(drmSession);
- }
- } finally {
- try {
- if (pendingDrmSession != null && pendingDrmSession != drmSession) {
- drmSessionManager.releaseSession(pendingDrmSession);
- }
- } finally {
- drmSession = null;
- pendingDrmSession = null;
- decoderCounters.ensureUpdated();
- eventDispatcher.disabled(decoderCounters);
- }
- }
+ eventDispatcher.disabled(decoderCounters);
}
}
@@ -612,12 +601,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return;
}
- drmSession = pendingDrmSession;
+ setDecoderDrmSession(sourceDrmSession);
+
ExoMediaCrypto mediaCrypto = null;
- if (drmSession != null) {
- mediaCrypto = drmSession.getMediaCrypto();
+ if (decoderDrmSession != null) {
+ mediaCrypto = decoderDrmSession.getMediaCrypto();
if (mediaCrypto == null) {
- DrmSessionException drmError = drmSession.getError();
+ DrmSessionException drmError = decoderDrmSession.getError();
if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a new
// input format causes the session to be replaced before it's used.
@@ -643,17 +633,34 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
private void releaseDecoder() {
- if (decoder == null) {
- return;
- }
-
inputBuffer = null;
outputBuffer = null;
- decoder.release();
- decoder = null;
- decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false;
+ if (decoder != null) {
+ decoder.release();
+ decoder = null;
+ decoderCounters.decoderReleaseCount++;
+ }
+ setDecoderDrmSession(null);
+ }
+
+ private void setSourceDrmSession(@Nullable DrmSession session) {
+ DrmSession previous = sourceDrmSession;
+ sourceDrmSession = session;
+ releaseDrmSessionIfUnused(previous);
+ }
+
+ private void setDecoderDrmSession(@Nullable DrmSession session) {
+ DrmSession previous = decoderDrmSession;
+ decoderDrmSession = session;
+ releaseDrmSessionIfUnused(previous);
+ }
+
+ private void releaseDrmSessionIfUnused(@Nullable DrmSession session) {
+ if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
+ drmSessionManager.releaseSession(session);
+ }
}
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
@@ -668,13 +675,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
}
- pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(),
- inputFormat.drmInitData);
- if (pendingDrmSession == drmSession) {
- drmSessionManager.releaseSession(pendingDrmSession);
+ DrmSession session =
+ drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
+ if (session == decoderDrmSession || session == sourceDrmSession) {
+ // We already had this session. The manager must be reference counting, so release it once
+ // to get the count attributed to this renderer back down to 1.
+ drmSessionManager.releaseSession(session);
}
+ setSourceDrmSession(session);
} else {
- pendingDrmSession = null;
+ setSourceDrmSession(null);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/DatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/DatabaseProvider.java
new file mode 100644
index 0000000000..2bb5f260ba
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/database/DatabaseProvider.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.database;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+
+/**
+ * Provides {@link SQLiteDatabase} instances to ExoPlayer components, which may read and write
+ * tables prefixed with {@link #TABLE_PREFIX}.
+ */
+public interface DatabaseProvider {
+
+ /** Prefix for tables that can be read and written by ExoPlayer components. */
+ String TABLE_PREFIX = "ExoPlayer";
+
+ /**
+ * Creates and/or opens a database that will be used for reading and writing.
+ *
+ * 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.
+ *
+ *
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();
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java
new file mode 100644
index 0000000000..c04683b434
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.database;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+/** A {@link DatabaseProvider} that provides instances obtained from a {@link SQLiteOpenHelper}. */
+public final class DefaultDatabaseProvider implements DatabaseProvider {
+
+ private final SQLiteOpenHelper sqliteOpenHelper;
+
+ /**
+ * @param sqliteOpenHelper An {@link SQLiteOpenHelper} from which to obtain database instances.
+ */
+ public DefaultDatabaseProvider(SQLiteOpenHelper sqliteOpenHelper) {
+ this.sqliteOpenHelper = sqliteOpenHelper;
+ }
+
+ @Override
+ public SQLiteDatabase getWritableDatabase() {
+ return sqliteOpenHelper.getWritableDatabase();
+ }
+
+ @Override
+ public SQLiteDatabase getReadableDatabase() {
+ return sqliteOpenHelper.getReadableDatabase();
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java
new file mode 100644
index 0000000000..e5bdfbb499
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.database;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import com.google.android.exoplayer2.util.Log;
+
+/**
+ * An {@link SQLiteOpenHelper} that provides instances of a standalone ExoPlayer database.
+ *
+ *
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);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java
new file mode 100644
index 0000000000..0b6ef3d816
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.database;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.support.annotation.IntDef;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A table that holds version information about other ExoPlayer tables. This allows ExoPlayer tables
+ * to be versioned independently to the version of the containing database.
+ */
+public final class VersionTable {
+
+ /** Returned by {@link #getVersion(int)} if the version is unset. */
+ public static final int VERSION_UNSET = -1;
+ /** Version of tables used for offline functionality. */
+ public static final int FEATURE_OFFLINE = 0;
+ /** Version of tables used for cache functionality. */
+ public static final int FEATURE_CACHE = 1;
+
+ private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Versions";
+
+ private static final String COLUMN_FEATURE = "feature";
+ private static final String COLUMN_VERSION = "version";
+
+ private static final String SQL_CREATE_TABLE_IF_NOT_EXISTS =
+ "CREATE TABLE IF NOT EXISTS "
+ + TABLE_NAME
+ + " ("
+ + COLUMN_FEATURE
+ + " INTEGER PRIMARY KEY NOT NULL,"
+ + COLUMN_VERSION
+ + " INTEGER NOT NULL)";
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FEATURE_OFFLINE, FEATURE_CACHE})
+ private @interface Feature {}
+
+ private final DatabaseProvider databaseProvider;
+
+ public VersionTable(DatabaseProvider databaseProvider) {
+ this.databaseProvider = databaseProvider;
+ // Check whether the table exists to avoid getting a writable database if we don't need one.
+ if (!doesTableExist(databaseProvider, TABLE_NAME)) {
+ databaseProvider.getWritableDatabase().execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS);
+ }
+ }
+
+ /**
+ * Sets the version of tables belonging to the specified feature.
+ *
+ * @param feature The feature.
+ * @param version The version.
+ */
+ public void setVersion(@Feature int feature, int version) {
+ ContentValues values = new ContentValues();
+ values.put(COLUMN_FEATURE, feature);
+ values.put(COLUMN_VERSION, version);
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
+ }
+
+ /**
+ * Returns the version of tables belonging to the specified feature, or {@link #VERSION_UNSET} if
+ * no version information is available.
+ */
+ public int getVersion(@Feature int feature) {
+ String selection = COLUMN_FEATURE + " = ?";
+ String[] selectionArgs = {Integer.toString(feature)};
+ try (Cursor cursor =
+ databaseProvider
+ .getReadableDatabase()
+ .query(
+ TABLE_NAME,
+ new String[] {COLUMN_VERSION},
+ selection,
+ selectionArgs,
+ /* groupBy= */ null,
+ /* having= */ null,
+ /* orderBy= */ null)) {
+ if (cursor.getCount() == 0) {
+ return VERSION_UNSET;
+ }
+ cursor.moveToNext();
+ return cursor.getInt(/* COLUMN_VERSION index */ 0);
+ }
+ }
+
+ /* package */ static boolean doesTableExist(DatabaseProvider databaseProvider, String tableName) {
+ SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
+ long count =
+ DatabaseUtils.queryNumEntries(
+ readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName});
+ return count > 0;
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java
index d5a4f6add5..feba7eaaf4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java
@@ -15,14 +15,5 @@
*/
package com.google.android.exoplayer2.drm;
-/**
- * An opaque {@link android.media.MediaCrypto} equivalent.
- */
-public interface ExoMediaCrypto {
-
- /**
- * @see android.media.MediaCrypto#requiresSecureDecoderComponent(String)
- */
- boolean requiresSecureDecoderComponent(String mimeType);
-
-}
+/** An opaque {@link android.media.MediaCrypto} equivalent. */
+public interface ExoMediaCrypto {}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
index 24c3ddbbd0..aca56139de 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
@@ -265,11 +265,9 @@ public interface ExoMediaDrm {
/**
* @see android.media.MediaCrypto#MediaCrypto(UUID, byte[])
- *
- * @param initData Opaque initialization data specific to the crypto scheme.
+ * @param sessionId The DRM session ID.
* @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.
* @throws MediaCryptoException If the instance can't be created.
*/
- T createMediaCrypto(byte[] initData) throws MediaCryptoException;
-
+ T createMediaCrypto(byte[] sessionId) throws MediaCryptoException;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
index 4e58ed6a31..156138ab9b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
@@ -17,48 +17,35 @@ package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi;
import android.media.MediaCrypto;
-import com.google.android.exoplayer2.util.Assertions;
+import java.util.UUID;
/**
- * An {@link ExoMediaCrypto} implementation that wraps the framework {@link MediaCrypto}.
+ * An {@link ExoMediaCrypto} implementation that contains the necessary information to build or
+ * update a framework {@link MediaCrypto}.
*/
@TargetApi(16)
public final class FrameworkMediaCrypto implements ExoMediaCrypto {
- private final MediaCrypto mediaCrypto;
- private final boolean forceAllowInsecureDecoderComponents;
+ /** The DRM scheme UUID. */
+ public final UUID uuid;
+ /** The DRM session id. */
+ public final byte[] sessionId;
+ /**
+ * Whether to allow use of insecure decoder components even if the underlying platform says
+ * otherwise.
+ */
+ public final boolean forceAllowInsecureDecoderComponents;
/**
- * @param mediaCrypto The {@link MediaCrypto} to wrap.
+ * @param uuid The DRM scheme UUID.
+ * @param sessionId The DRM session id.
+ * @param forceAllowInsecureDecoderComponents Whether to allow use of insecure decoder components
+ * even if the underlying platform says otherwise.
*/
- public FrameworkMediaCrypto(MediaCrypto mediaCrypto) {
- this(mediaCrypto, false);
- }
-
- /**
- * @param mediaCrypto The {@link MediaCrypto} to wrap.
- * @param forceAllowInsecureDecoderComponents Whether to force
- * {@link #requiresSecureDecoderComponent(String)} to return {@code false}, rather than
- * {@link MediaCrypto#requiresSecureDecoderComponent(String)} of the wrapped
- * {@link MediaCrypto}.
- */
- public FrameworkMediaCrypto(MediaCrypto mediaCrypto,
- boolean forceAllowInsecureDecoderComponents) {
- this.mediaCrypto = Assertions.checkNotNull(mediaCrypto);
+ public FrameworkMediaCrypto(
+ UUID uuid, byte[] sessionId, boolean forceAllowInsecureDecoderComponents) {
+ this.uuid = uuid;
+ this.sessionId = sessionId;
this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;
}
-
- /**
- * Returns the wrapped {@link MediaCrypto}.
- */
- public MediaCrypto getWrappedMediaCrypto() {
- return mediaCrypto;
- }
-
- @Override
- public boolean requiresSecureDecoderComponent(String mimeType) {
- return !forceAllowInsecureDecoderComponents
- && mediaCrypto.requiresSecureDecoderComponent(mimeType);
- }
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
index fda85a759c..b139288f98 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.drm;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.DeniedByServerException;
-import android.media.MediaCrypto;
import android.media.MediaCryptoException;
import android.media.MediaDrm;
import android.media.MediaDrmException;
@@ -210,7 +209,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm schemeDatas) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java
index ab49ca5454..87bb992082 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java
@@ -34,16 +34,26 @@ public final class MpegAudioHeader {
private static final String[] MIME_TYPE_BY_LAYER =
new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG};
private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000};
- private static final int[] BITRATE_V1_L1 =
- {32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448};
- private static final int[] BITRATE_V2_L1 =
- {32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256};
- private static final int[] BITRATE_V1_L2 =
- {32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384};
- private static final int[] BITRATE_V1_L3 =
- {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320};
- private static final int[] BITRATE_V2 =
- {8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160};
+ private static final int[] BITRATE_V1_L1 = {
+ 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000,
+ 416000, 448000
+ };
+ private static final int[] BITRATE_V2_L1 = {
+ 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000,
+ 224000, 256000
+ };
+ private static final int[] BITRATE_V1_L2 = {
+ 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000,
+ 320000, 384000
+ };
+ private static final int[] BITRATE_V1_L3 = {
+ 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000,
+ 320000
+ };
+ private static final int[] BITRATE_V2 = {
+ 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000,
+ 160000
+ };
/**
* Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it
@@ -89,7 +99,7 @@ public final class MpegAudioHeader {
if (layer == 3) {
// Layer I (layer == 3)
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
- return (12000 * bitrate / samplingRate + padding) * 4;
+ return (12 * bitrate / samplingRate + padding) * 4;
} else {
// Layer II (layer == 2) or III (layer == 1)
if (version == 3) {
@@ -102,10 +112,10 @@ public final class MpegAudioHeader {
if (version == 3) {
// Version 1
- return 144000 * bitrate / samplingRate + padding;
+ return 144 * bitrate / samplingRate + padding;
} else {
// Version 2 or 2.5
- return (layer == 1 ? 72000 : 144000) * bitrate / samplingRate + padding;
+ return (layer == 1 ? 72 : 144) * bitrate / samplingRate + padding;
}
}
@@ -159,7 +169,7 @@ public final class MpegAudioHeader {
if (layer == 3) {
// Layer I (layer == 3)
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
- frameSize = (12000 * bitrate / sampleRate + padding) * 4;
+ frameSize = (12 * bitrate / sampleRate + padding) * 4;
samplesPerFrame = 384;
} else {
// Layer II (layer == 2) or III (layer == 1)
@@ -167,19 +177,22 @@ public final class MpegAudioHeader {
// Version 1
bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
samplesPerFrame = 1152;
- frameSize = 144000 * bitrate / sampleRate + padding;
+ frameSize = 144 * bitrate / sampleRate + padding;
} else {
// Version 2 or 2.5.
bitrate = BITRATE_V2[bitrateIndex - 1];
samplesPerFrame = layer == 1 ? 576 : 1152;
- frameSize = (layer == 1 ? 72000 : 144000) * bitrate / sampleRate + padding;
+ frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding;
}
}
+ // Calculate the bitrate in the same way Mp3Extractor calculates sample timestamps so that
+ // seeking to a given timestamp and playing from the start up to that timestamp give the same
+ // results for CBR streams. See also [internal: b/120390268].
+ bitrate = 8 * frameSize * sampleRate / samplesPerFrame;
String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
- header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000,
- samplesPerFrame);
+ header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame);
return true;
}
@@ -198,8 +211,14 @@ public final class MpegAudioHeader {
/** Number of samples stored in the frame. */
public int samplesPerFrame;
- private void setValues(int version, String mimeType, int frameSize, int sampleRate, int channels,
- int bitrate, int samplesPerFrame) {
+ private void setValues(
+ int version,
+ String mimeType,
+ int frameSize,
+ int sampleRate,
+ int channels,
+ int bitrate,
+ int samplesPerFrame) {
this.version = version;
this.mimeType = mimeType;
this.frameSize = frameSize;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
index 86b750e821..187b9ae443 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
@@ -191,7 +191,11 @@ public final class MatroskaExtractor implements Extractor {
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
private static final int ID_LANGUAGE = 0x22B59C;
private static final int ID_PROJECTION = 0x7670;
+ private static final int ID_PROJECTION_TYPE = 0x7671;
private static final int ID_PROJECTION_PRIVATE = 0x7672;
+ private static final int ID_PROJECTION_POSE_YAW = 0x7673;
+ private static final int ID_PROJECTION_POSE_PITCH = 0x7674;
+ private static final int ID_PROJECTION_POSE_ROLL = 0x7675;
private static final int ID_STEREO_MODE = 0x53B8;
private static final int ID_COLOUR = 0x55B0;
private static final int ID_COLOUR_RANGE = 0x55B9;
@@ -760,6 +764,24 @@ public final class MatroskaExtractor implements Extractor {
case ID_MAX_FALL:
currentTrack.maxFrameAverageLuminance = (int) value;
break;
+ case ID_PROJECTION_TYPE:
+ switch ((int) value) {
+ case 0:
+ currentTrack.projectionType = C.PROJECTION_RECTANGULAR;
+ break;
+ case 1:
+ currentTrack.projectionType = C.PROJECTION_EQUIRECTANGULAR;
+ break;
+ case 2:
+ currentTrack.projectionType = C.PROJECTION_CUBEMAP;
+ break;
+ case 3:
+ currentTrack.projectionType = C.PROJECTION_MESH;
+ break;
+ default:
+ break;
+ }
+ break;
default:
break;
}
@@ -803,6 +825,15 @@ public final class MatroskaExtractor implements Extractor {
case ID_LUMNINANCE_MIN:
currentTrack.minMasteringLuminance = (float) value;
break;
+ case ID_PROJECTION_POSE_YAW:
+ currentTrack.projectionPoseYaw = (float) value;
+ break;
+ case ID_PROJECTION_POSE_PITCH:
+ currentTrack.projectionPosePitch = (float) value;
+ break;
+ case ID_PROJECTION_POSE_ROLL:
+ currentTrack.projectionPoseRoll = (float) value;
+ break;
default:
break;
}
@@ -1465,6 +1496,7 @@ public final class MatroskaExtractor implements Extractor {
case ID_COLOUR_PRIMARIES:
case ID_MAX_CLL:
case ID_MAX_FALL:
+ case ID_PROJECTION_TYPE:
return TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
case ID_NAME:
@@ -1491,6 +1523,9 @@ public final class MatroskaExtractor implements Extractor {
case ID_WHITE_POINT_CHROMATICITY_Y:
case ID_LUMNINANCE_MAX:
case ID_LUMNINANCE_MIN:
+ case ID_PROJECTION_POSE_YAW:
+ case ID_PROJECTION_POSE_PITCH:
+ case ID_PROJECTION_POSE_ROLL:
return TYPE_FLOAT;
default:
return TYPE_UNKNOWN;
@@ -1631,6 +1666,10 @@ public final class MatroskaExtractor implements Extractor {
public int displayWidth = Format.NO_VALUE;
public int displayHeight = Format.NO_VALUE;
public int displayUnit = DISPLAY_UNIT_PIXELS;
+ @C.Projection public int projectionType = Format.NO_VALUE;
+ public float projectionPoseYaw = 0f;
+ public float projectionPosePitch = 0f;
+ public float projectionPoseRoll = 0f;
public byte[] projectionData = null;
@C.StereoMode
public int stereoMode = Format.NO_VALUE;
@@ -1850,6 +1889,21 @@ public final class MatroskaExtractor implements Extractor {
} else if ("htc_video_rotA-270".equals(name)) {
rotationDegrees = 270;
}
+ if (projectionType == C.PROJECTION_RECTANGULAR
+ && Float.compare(projectionPoseYaw, 0f) == 0
+ && Float.compare(projectionPosePitch, 0f) == 0) {
+ // The range of projectionPoseRoll is [-180, 180].
+ if (Float.compare(projectionPoseRoll, 0f) == 0) {
+ rotationDegrees = 0;
+ } else if (Float.compare(projectionPosePitch, 90f) == 0) {
+ rotationDegrees = 90;
+ } else if (Float.compare(projectionPosePitch, -180f) == 0
+ || Float.compare(projectionPosePitch, 180f) == 0) {
+ rotationDegrees = 180;
+ } else if (Float.compare(projectionPosePitch, -90f) == 0) {
+ rotationDegrees = 270;
+ }
+ }
format =
Format.createVideoSampleFormat(
Integer.toString(trackId),
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
index 440e577c7d..8d78337617 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java
@@ -22,7 +22,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-@SuppressWarnings("ConstantField")
+@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"})
/* package */ abstract class Atom {
/**
@@ -130,6 +130,7 @@ import java.util.List;
public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb");
public static final int TYPE_udta = Util.getIntegerCodeForString("udta");
public static final int TYPE_meta = Util.getIntegerCodeForString("meta");
+ public static final int TYPE_keys = Util.getIntegerCodeForString("keys");
public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst");
public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
public static final int TYPE_name = Util.getIntegerCodeForString("name");
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
index d085156f2b..008a155d1f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.mp4;
import static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType;
+import android.support.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@@ -39,7 +40,7 @@ import java.util.Collections;
import java.util.List;
/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */
-@SuppressWarnings("ConstantField")
+@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"})
/* package */ final class AtomParsers {
private static final String TAG = "AtomParsers";
@@ -51,6 +52,7 @@ import java.util.List;
private static final int TYPE_subt = Util.getIntegerCodeForString("subt");
private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
private static final int TYPE_meta = Util.getIntegerCodeForString("meta");
+ private static final int TYPE_mdta = Util.getIntegerCodeForString("mdta");
/**
* The threshold number of samples to trim from the start/end of an audio track when applying an
@@ -77,7 +79,7 @@ import java.util.List;
DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime)
throws ParserException {
Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
- int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
+ int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data));
if (trackType == C.TRACK_TYPE_UNKNOWN) {
return null;
}
@@ -485,6 +487,7 @@ import java.util.List;
* @param isQuickTime True for QuickTime media. False otherwise.
* @return Parsed metadata, or null.
*/
+ @Nullable
public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
if (isQuickTime) {
// Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
@@ -499,14 +502,69 @@ import java.util.List;
int atomType = udtaData.readInt();
if (atomType == Atom.TYPE_meta) {
udtaData.setPosition(atomPosition);
- return parseMetaAtom(udtaData, atomPosition + atomSize);
+ return parseUdtaMeta(udtaData, atomPosition + atomSize);
}
- udtaData.skipBytes(atomSize - Atom.HEADER_SIZE);
+ udtaData.setPosition(atomPosition + atomSize);
}
return null;
}
- private static Metadata parseMetaAtom(ParsableByteArray meta, int limit) {
+ /**
+ * Parses a metadata meta atom if it contains metadata with handler 'mdta'.
+ *
+ * @param meta The metadata atom to decode.
+ * @return Parsed metadata, or null.
+ */
+ @Nullable
+ public static Metadata parseMdtaFromMeta(Atom.ContainerAtom meta) {
+ Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr);
+ Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys);
+ Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst);
+ if (hdlrAtom == null
+ || keysAtom == null
+ || ilstAtom == null
+ || AtomParsers.parseHdlr(hdlrAtom.data) != TYPE_mdta) {
+ // There isn't enough information to parse the metadata, or the handler type is unexpected.
+ return null;
+ }
+
+ // Parse metadata keys.
+ ParsableByteArray keys = keysAtom.data;
+ keys.setPosition(Atom.FULL_HEADER_SIZE);
+ int entryCount = keys.readInt();
+ String[] keyNames = new String[entryCount];
+ for (int i = 0; i < entryCount; i++) {
+ int entrySize = keys.readInt();
+ keys.skipBytes(4); // keyNamespace
+ int keySize = entrySize - 8;
+ keyNames[i] = keys.readString(keySize);
+ }
+
+ // Parse metadata items.
+ ParsableByteArray ilst = ilstAtom.data;
+ ilst.setPosition(Atom.HEADER_SIZE);
+ ArrayList entries = new ArrayList<>();
+ while (ilst.bytesLeft() > Atom.HEADER_SIZE) {
+ int atomPosition = ilst.getPosition();
+ int atomSize = ilst.readInt();
+ int keyIndex = ilst.readInt() - 1;
+ if (keyIndex >= 0 && keyIndex < keyNames.length) {
+ String key = keyNames[keyIndex];
+ Metadata.Entry entry =
+ MetadataUtil.parseMdtaMetadataEntryFromIlst(ilst, atomPosition + atomSize, key);
+ if (entry != null) {
+ entries.add(entry);
+ }
+ } else {
+ Log.w(TAG, "Skipped metadata with unknown key index: " + keyIndex);
+ }
+ ilst.setPosition(atomPosition + atomSize);
+ }
+ return entries.isEmpty() ? null : new Metadata(entries);
+ }
+
+ @Nullable
+ private static Metadata parseUdtaMeta(ParsableByteArray meta, int limit) {
meta.skipBytes(Atom.FULL_HEADER_SIZE);
while (meta.getPosition() < limit) {
int atomPosition = meta.getPosition();
@@ -516,11 +574,12 @@ import java.util.List;
meta.setPosition(atomPosition);
return parseIlst(meta, atomPosition + atomSize);
}
- meta.skipBytes(atomSize - Atom.HEADER_SIZE);
+ meta.setPosition(atomPosition + atomSize);
}
return null;
}
+ @Nullable
private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
ilst.skipBytes(Atom.HEADER_SIZE);
ArrayList entries = new ArrayList<>();
@@ -610,19 +669,22 @@ import java.util.List;
* Parses an hdlr atom.
*
* @param hdlr The hdlr atom to decode.
- * @return The track type.
+ * @return The handler value.
*/
private static int parseHdlr(ParsableByteArray hdlr) {
hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
- int trackType = hdlr.readInt();
- if (trackType == TYPE_soun) {
+ return hdlr.readInt();
+ }
+
+ /** Returns the track type for a given handler value. */
+ private static int getTrackTypeForHdlr(int hdlr) {
+ if (hdlr == TYPE_soun) {
return C.TRACK_TYPE_AUDIO;
- } else if (trackType == TYPE_vide) {
+ } else if (hdlr == TYPE_vide) {
return C.TRACK_TYPE_VIDEO;
- } else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt
- || trackType == TYPE_clcp) {
+ } else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) {
return C.TRACK_TYPE_TEXT;
- } else if (trackType == TYPE_meta) {
+ } else if (hdlr == TYPE_meta) {
return C.TRACK_TYPE_METADATA;
} else {
return C.TRACK_TYPE_UNKNOWN;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java
new file mode 100644
index 0000000000..b458a8f0f4
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mp4;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/**
+ * Stores extensible metadata with handler type 'mdta'. See also the QuickTime File Format
+ * Specification.
+ */
+public final class MdtaMetadataEntry implements Metadata.Entry {
+
+ /** The metadata key name. */
+ public final String key;
+ /** The payload. The interpretation of the value depends on {@link #typeIndicator}. */
+ public final byte[] value;
+ /** The four byte locale indicator. */
+ public final int localeIndicator;
+ /** The four byte type indicator. */
+ public final int typeIndicator;
+
+ /** Creates a new metadata entry for the specified metadata key/value. */
+ public MdtaMetadataEntry(String key, byte[] value, int localeIndicator, int typeIndicator) {
+ this.key = key;
+ this.value = value;
+ this.localeIndicator = localeIndicator;
+ this.typeIndicator = typeIndicator;
+ }
+
+ private MdtaMetadataEntry(Parcel in) {
+ key = Util.castNonNull(in.readString());
+ value = new byte[in.readInt()];
+ in.readByteArray(value);
+ localeIndicator = in.readInt();
+ typeIndicator = in.readInt();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ MdtaMetadataEntry other = (MdtaMetadataEntry) obj;
+ return key.equals(other.key)
+ && Arrays.equals(value, other.value)
+ && localeIndicator == other.localeIndicator
+ && typeIndicator == other.typeIndicator;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + key.hashCode();
+ result = 31 * result + Arrays.hashCode(value);
+ result = 31 * result + localeIndicator;
+ result = 31 * result + typeIndicator;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "mdta: key=" + key;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(key);
+ dest.writeInt(value.length);
+ dest.writeByteArray(value);
+ dest.writeInt(localeIndicator);
+ dest.writeInt(typeIndicator);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+
+ @Override
+ public MdtaMetadataEntry createFromParcel(Parcel in) {
+ return new MdtaMetadataEntry(in);
+ }
+
+ @Override
+ public MdtaMetadataEntry[] newArray(int size) {
+ return new MdtaMetadataEntry[size];
+ }
+ };
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
index 670fe116a6..02522897ce 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
@@ -16,6 +16,9 @@
package com.google.android.exoplayer2.extractor.mp4;
import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
@@ -25,10 +28,9 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
-/**
- * Parses metadata items stored in ilst atoms.
- */
+/** Utilities for handling metadata in MP4. */
/* package */ final class MetadataUtil {
private static final String TAG = "MetadataUtil";
@@ -103,24 +105,73 @@ import com.google.android.exoplayer2.util.Util;
private static final String LANGUAGE_UNDEFINED = "und";
+ private static final int TYPE_TOP_BYTE_COPYRIGHT = 0xA9;
+ private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD.
+
+ private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps";
+ private static final int MDTA_TYPE_INDICATOR_FLOAT = 23;
+
private MetadataUtil() {}
/**
- * Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting
- * from the current position of the {@link ParsableByteArray}, and the position is advanced by the
- * size of the element. The position is advanced even if the element's type is unrecognized.
+ * Returns a {@link Format} that is the same as the input format but includes information from the
+ * specified sources of metadata.
+ */
+ public static Format getFormatWithMetadata(
+ int trackType,
+ Format format,
+ @Nullable Metadata udtaMetadata,
+ @Nullable Metadata mdtaMetadata,
+ GaplessInfoHolder gaplessInfoHolder) {
+ if (trackType == C.TRACK_TYPE_AUDIO) {
+ if (gaplessInfoHolder.hasGaplessInfo()) {
+ format =
+ format.copyWithGaplessInfo(
+ gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding);
+ }
+ // We assume all udta metadata is associated with the audio track.
+ if (udtaMetadata != null) {
+ format = format.copyWithMetadata(udtaMetadata);
+ }
+ } else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) {
+ // Populate only metadata keys that are known to be specific to video.
+ for (int i = 0; i < mdtaMetadata.length(); i++) {
+ Metadata.Entry entry = mdtaMetadata.get(i);
+ if (entry instanceof MdtaMetadataEntry) {
+ MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry;
+ if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key)
+ && mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) {
+ try {
+ float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get();
+ format = format.copyWithFrameRate(fps);
+ format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry));
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ignoring invalid framerate");
+ }
+ }
+ }
+ }
+ }
+ return format;
+ }
+
+ /**
+ * Parses a single userdata ilst element from a {@link ParsableByteArray}. The element is read
+ * starting from the current position of the {@link ParsableByteArray}, and the position is
+ * advanced by the size of the element. The position is advanced even if the element's type is
+ * unrecognized.
*
* @param ilst Holds the data to be parsed.
* @return The parsed element, or null if the element's type was not recognized.
*/
- public static @Nullable Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
+ @Nullable
+ public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
int position = ilst.getPosition();
int endPosition = position + ilst.readInt();
int type = ilst.readInt();
int typeTopByte = (type >> 24) & 0xFF;
try {
- if (typeTopByte == '\u00A9' /* Copyright char */
- || typeTopByte == '\uFFFD' /* Replacement char */) {
+ if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) {
int shortType = type & 0x00FFFFFF;
if (shortType == SHORT_TYPE_COMMENT) {
return parseCommentAttribute(type, ilst);
@@ -185,7 +236,36 @@ import com.google.android.exoplayer2.util.Util;
}
}
- private static @Nullable TextInformationFrame parseTextAttribute(
+ /**
+ * Parses an 'mdta' metadata entry starting at the current position in an ilst box.
+ *
+ * @param ilst The ilst box.
+ * @param endPosition The end position of the entry in the ilst box.
+ * @param key The mdta metadata entry key for the entry.
+ * @return The parsed element, or null if the entry wasn't recognized.
+ */
+ @Nullable
+ public static MdtaMetadataEntry parseMdtaMetadataEntryFromIlst(
+ ParsableByteArray ilst, int endPosition, String key) {
+ int atomPosition;
+ while ((atomPosition = ilst.getPosition()) < endPosition) {
+ int atomSize = ilst.readInt();
+ int atomType = ilst.readInt();
+ if (atomType == Atom.TYPE_data) {
+ int typeIndicator = ilst.readInt();
+ int localeIndicator = ilst.readInt();
+ int dataSize = atomSize - 16;
+ byte[] value = new byte[dataSize];
+ ilst.readBytes(value, 0, dataSize);
+ return new MdtaMetadataEntry(key, value, localeIndicator, typeIndicator);
+ }
+ ilst.setPosition(atomPosition + atomSize);
+ }
+ return null;
+ }
+
+ @Nullable
+ private static TextInformationFrame parseTextAttribute(
int type, String id, ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
@@ -198,7 +278,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
- private static @Nullable CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
+ @Nullable
+ private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data) {
@@ -210,7 +291,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
- private static @Nullable Id3Frame parseUint8Attribute(
+ @Nullable
+ private static Id3Frame parseUint8Attribute(
int type,
String id,
ParsableByteArray data,
@@ -229,7 +311,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
- private static @Nullable TextInformationFrame parseIndexAndCountAttribute(
+ @Nullable
+ private static TextInformationFrame parseIndexAndCountAttribute(
int type, String attributeName, ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
@@ -249,8 +332,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
- private static @Nullable TextInformationFrame parseStandardGenreAttribute(
- ParsableByteArray data) {
+ @Nullable
+ private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {
int genreCode = parseUint8AttributeValue(data);
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
? STANDARD_GENRES[genreCode - 1] : null;
@@ -261,7 +344,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
- private static @Nullable ApicFrame parseCoverArt(ParsableByteArray data) {
+ @Nullable
+ private static ApicFrame parseCoverArt(ParsableByteArray data) {
int atomSize = data.readInt();
int atomType = data.readInt();
if (atomType == Atom.TYPE_data) {
@@ -285,8 +369,8 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
- private static @Nullable Id3Frame parseInternalAttribute(
- ParsableByteArray data, int endPosition) {
+ @Nullable
+ private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) {
String domain = null;
String name = null;
int dataAtomPosition = -1;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
index ec24bed964..5356fdb548 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
@@ -75,7 +75,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private static final int STATE_READING_ATOM_PAYLOAD = 1;
private static final int STATE_READING_SAMPLE = 2;
- // Brand stored in the ftyp atom for QuickTime media.
+ /** Brand stored in the ftyp atom for QuickTime media. */
private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt ");
/**
@@ -377,15 +377,21 @@ public final class Mp4Extractor implements Extractor, SeekMap {
long durationUs = C.TIME_UNSET;
List tracks = new ArrayList<>();
- Metadata metadata = null;
+ // Process metadata.
+ Metadata udtaMetadata = null;
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
if (udta != null) {
- metadata = AtomParsers.parseUdta(udta, isQuickTime);
- if (metadata != null) {
- gaplessInfoHolder.setFromMetadata(metadata);
+ udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime);
+ if (udtaMetadata != null) {
+ gaplessInfoHolder.setFromMetadata(udtaMetadata);
}
}
+ Metadata mdtaMetadata = null;
+ Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta);
+ if (meta != null) {
+ mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta);
+ }
boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
ArrayList trackSampleTables =
@@ -401,15 +407,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
// Allow ten source samples per output sample, like the platform extractor.
int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
Format format = track.format.copyWithMaxInputSize(maxInputSize);
- if (track.type == C.TRACK_TYPE_AUDIO) {
- if (gaplessInfoHolder.hasGaplessInfo()) {
- format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
- gaplessInfoHolder.encoderPadding);
- }
- if (metadata != null) {
- format = format.copyWithMetadata(metadata);
- }
- }
+ format =
+ MetadataUtil.getFormatWithMetadata(
+ track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder);
mp4Track.trackOutput.format(format);
durationUs =
@@ -716,24 +716,37 @@ public final class Mp4Extractor implements Extractor, SeekMap {
return false;
}
- /**
- * Returns whether the extractor should decode a leaf atom with type {@code atom}.
- */
+ /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */
private static boolean shouldParseLeafAtom(int atom) {
- return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr
- || atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss
- || atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc
- || atom == Atom.TYPE_stsz || atom == Atom.TYPE_stz2 || atom == Atom.TYPE_stco
- || atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp
- || atom == Atom.TYPE_udta;
+ return atom == Atom.TYPE_mdhd
+ || atom == Atom.TYPE_mvhd
+ || atom == Atom.TYPE_hdlr
+ || atom == Atom.TYPE_stsd
+ || atom == Atom.TYPE_stts
+ || atom == Atom.TYPE_stss
+ || atom == Atom.TYPE_ctts
+ || atom == Atom.TYPE_elst
+ || atom == Atom.TYPE_stsc
+ || atom == Atom.TYPE_stsz
+ || atom == Atom.TYPE_stz2
+ || atom == Atom.TYPE_stco
+ || atom == Atom.TYPE_co64
+ || atom == Atom.TYPE_tkhd
+ || atom == Atom.TYPE_ftyp
+ || atom == Atom.TYPE_udta
+ || atom == Atom.TYPE_keys
+ || atom == Atom.TYPE_ilst;
}
- /**
- * Returns whether the extractor should decode a container atom with type {@code atom}.
- */
+ /** Returns whether the extractor should decode a container atom with type {@code atom}. */
private static boolean shouldParseContainerAtom(int atom) {
- return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
- || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_edts;
+ return atom == Atom.TYPE_moov
+ || atom == Atom.TYPE_trak
+ || atom == Atom.TYPE_mdia
+ || atom == Atom.TYPE_minf
+ || atom == Atom.TYPE_stbl
+ || atom == Atom.TYPE_edts
+ || atom == Atom.TYPE_meta;
}
private static final class Mp4Track {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
index 021c9de654..a1c90bf1f2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
@@ -27,9 +27,7 @@ import java.io.IOException;
*/
/* package */ final class Sniffer {
- /**
- * The maximum number of bytes to peek when sniffing.
- */
+ /** The maximum number of bytes to peek when sniffing. */
private static final int SEARCH_LENGTH = 4 * 1024;
private static final int[] COMPATIBLE_BRANDS = new int[] {
@@ -109,15 +107,19 @@ import java.io.IOException;
headerSize = Atom.LONG_HEADER_SIZE;
input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
buffer.setLimit(Atom.LONG_HEADER_SIZE);
- atomSize = buffer.readUnsignedLongToLong();
+ atomSize = buffer.readLong();
} else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {
// The atom extends to the end of the file.
- long endPosition = input.getLength();
- if (endPosition != C.LENGTH_UNSET) {
- atomSize = endPosition - input.getPosition() + headerSize;
+ long fileEndPosition = input.getLength();
+ if (fileEndPosition != C.LENGTH_UNSET) {
+ atomSize = fileEndPosition - input.getPeekPosition() + headerSize;
}
}
+ if (inputLength != C.LENGTH_UNSET && bytesSearched + atomSize > inputLength) {
+ // The file is invalid because the atom extends past the end of the file.
+ return false;
+ }
if (atomSize < headerSize) {
// The file is invalid because the atom size is too small for its header.
return false;
@@ -125,6 +127,13 @@ import java.io.IOException;
bytesSearched += headerSize;
if (atomType == Atom.TYPE_moov) {
+ // We have seen the moov atom. We increase the search size to make sure we don't miss an
+ // mvex atom because the moov's size exceeds the search length.
+ bytesToSearch += (int) atomSize;
+ if (inputLength != C.LENGTH_UNSET && bytesToSearch > inputLength) {
+ // Make sure we don't exceed the file size.
+ bytesToSearch = (int) inputLength;
+ }
// Check for an mvex atom inside the moov atom to identify whether the file is fragmented.
continue;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
index 56851fc1e0..59ea386335 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
@@ -64,6 +64,9 @@ import com.google.android.exoplayer2.util.Util;
this.flags = flags;
this.durationUs = durationUs;
sampleCount = offsets.length;
+ if (flags.length > 0) {
+ flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE;
+ }
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
index 93ce15a7ab..3741d52294 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
+import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
+
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.audio.Ac3Util;
import com.google.android.exoplayer2.extractor.Extractor;
@@ -140,7 +142,7 @@ public final class Ac3Extractor implements Extractor {
if (!startedPacket) {
// Pass data to the reader as though it's contained within a single infinitely long packet.
- reader.packetStarted(firstSampleTimestampUs, true);
+ reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR);
startedPacket = true;
}
// TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java
index 2ef9704a7a..93724be92d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java
@@ -100,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
}
@Override
- public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
index 04a6b571bd..77b79fa19f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
+import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
+
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
@@ -202,7 +204,7 @@ public final class AdtsExtractor implements Extractor {
if (!startedPacket) {
// Pass data to the reader as though it's contained within a single infinitely long packet.
- reader.packetStarted(firstSampleTimestampUs, true);
+ reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR);
startedPacket = true;
}
// TODO: Make it possible for reader to consume the dataSource directly, so that it becomes
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
index e31f67c77c..589b543170 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
@@ -141,7 +141,7 @@ public final class AdtsReader implements ElementaryStreamReader {
}
@Override
- public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
index a5506e2cfb..88805d9362 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
@@ -50,7 +50,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
FLAG_IGNORE_H264_STREAM,
FLAG_DETECT_ACCESS_UNITS,
FLAG_IGNORE_SPLICE_INFO_STREAM,
- FLAG_OVERRIDE_CAPTION_DESCRIPTORS
+ FLAG_OVERRIDE_CAPTION_DESCRIPTORS,
+ FLAG_IGNORE_HDMV_DTS_STREAM
})
public @interface Flags {}
@@ -86,6 +87,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
* closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors.
*/
public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5;
+ /**
+ * Prevents the creation of {@link DtsReader} instances when receiving {@link
+ * TsExtractor#TS_STREAM_TYPE_HDMV_DTS} as stream type. Enabling this flag prevents a stream type
+ * collision between HDMV DTS audio and SCTE-35 subtitles.
+ */
+ public static final int FLAG_IGNORE_HDMV_DTS_STREAM = 1 << 6;
private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86;
@@ -142,8 +149,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
case TsExtractor.TS_STREAM_TYPE_AC3:
case TsExtractor.TS_STREAM_TYPE_E_AC3:
return new PesReader(new Ac3Reader(esInfo.language));
- case TsExtractor.TS_STREAM_TYPE_DTS:
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
+ if (isSet(FLAG_IGNORE_HDMV_DTS_STREAM)) {
+ return null;
+ }
+ // Fall through.
+ case TsExtractor.TS_STREAM_TYPE_DTS:
return new PesReader(new DtsReader(esInfo.language));
case TsExtractor.TS_STREAM_TYPE_H262:
return new PesReader(new H262Reader(buildUserDataReader(esInfo)));
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java
index 2e45853951..1f9b0e79d4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java
@@ -80,7 +80,7 @@ public final class DtsReader implements ElementaryStreamReader {
}
@Override
- public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java
index 0944d1810e..3f0a772b1c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
+import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
+
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@@ -73,8 +75,8 @@ public final class DvbSubtitleReader implements ElementaryStreamReader {
}
@Override
- public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
- if (!dataAlignmentIndicator) {
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
return;
}
writingSample = true;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
index fa7f78c8c0..e022fc237b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
@@ -43,9 +43,9 @@ public interface ElementaryStreamReader {
* Called when a packet starts.
*
* @param pesTimeUs The timestamp associated with the packet.
- * @param dataAlignmentIndicator The data alignment indicator associated with the packet.
+ * @param flags See {@link TsPayloadReader.Flags}.
*/
- void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator);
+ void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags);
/**
* Consumes (possibly partial) data from the current packet.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java
index e9827893ee..1564157d44 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java
@@ -107,7 +107,8 @@ public final class H262Reader implements ElementaryStreamReader {
}
@Override
- public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ // TODO (Internal b/32267012): Consider using random access indicator.
this.pesTimeUs = pesTimeUs;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java
index 45e094f69d..d249c1b9da 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
+import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR;
+
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@@ -56,9 +58,12 @@ public final class H264Reader implements ElementaryStreamReader {
// State that should not be reset on seek.
private boolean hasOutputFormat;
- // Per packet state that gets reset at the start of each packet.
+ // Per PES packet state that gets reset at the start of each PES packet.
private long pesTimeUs;
+ // State inherited from the TS packet header.
+ private boolean randomAccessIndicator;
+
// Scratch variables to avoid allocations.
private final ParsableByteArray seiWrapper;
@@ -88,6 +93,7 @@ public final class H264Reader implements ElementaryStreamReader {
sei.reset();
sampleReader.reset();
totalBytesWritten = 0;
+ randomAccessIndicator = false;
}
@Override
@@ -100,8 +106,9 @@ public final class H264Reader implements ElementaryStreamReader {
}
@Override
- public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
this.pesTimeUs = pesTimeUs;
+ randomAccessIndicator |= (flags & FLAG_RANDOM_ACCESS_INDICATOR) != 0;
}
@Override
@@ -220,12 +227,17 @@ public final class H264Reader implements ElementaryStreamReader {
seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.
seiReader.consume(pesTimeUs, seiWrapper);
}
- sampleReader.endNalUnit(position, offset);
+ boolean sampleIsKeyFrame =
+ sampleReader.endNalUnit(position, offset, hasOutputFormat, randomAccessIndicator);
+ if (sampleIsKeyFrame) {
+ // This is either an IDR frame or the first I-frame since the random access indicator, so mark
+ // it as a keyframe. Clear the flag so that subsequent non-IDR I-frames are not marked as
+ // keyframes until we see another random access indicator.
+ randomAccessIndicator = false;
+ }
}
- /**
- * Consumes a stream of NAL units and outputs samples.
- */
+ /** Consumes a stream of NAL units and outputs samples. */
private static final class SampleReader {
private static final int DEFAULT_BUFFER_SIZE = 128;
@@ -430,11 +442,12 @@ public final class H264Reader implements ElementaryStreamReader {
isFilling = false;
}
- public void endNalUnit(long position, int offset) {
+ public boolean endNalUnit(
+ long position, int offset, boolean hasOutputFormat, boolean randomAccessIndicator) {
if (nalUnitType == NAL_UNIT_TYPE_AUD
|| (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) {
// If the NAL unit ending is the start of a new sample, output the previous one.
- if (readingSample) {
+ if (hasOutputFormat && readingSample) {
int nalUnitLength = (int) (position - nalUnitStartPosition);
outputSample(offset + nalUnitLength);
}
@@ -443,8 +456,12 @@ public final class H264Reader implements ElementaryStreamReader {
sampleIsKeyframe = false;
readingSample = true;
}
- sampleIsKeyframe |= nalUnitType == NAL_UNIT_TYPE_IDR || (allowNonIdrKeyframes
- && nalUnitType == NAL_UNIT_TYPE_NON_IDR && sliceHeader.isISlice());
+ boolean treatIFrameAsKeyframe =
+ allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator;
+ sampleIsKeyframe |=
+ nalUnitType == NAL_UNIT_TYPE_IDR
+ || (treatIFrameAsKeyframe && nalUnitType == NAL_UNIT_TYPE_NON_IDR);
+ return sampleIsKeyframe;
}
private void outputSample(int offset) {
@@ -486,10 +503,21 @@ public final class H264Reader implements ElementaryStreamReader {
hasSliceType = true;
}
- public void setAll(SpsData spsData, int nalRefIdc, int sliceType, int frameNum,
- int picParameterSetId, boolean fieldPicFlag, boolean bottomFieldFlagPresent,
- boolean bottomFieldFlag, boolean idrPicFlag, int idrPicId, int picOrderCntLsb,
- int deltaPicOrderCntBottom, int deltaPicOrderCnt0, int deltaPicOrderCnt1) {
+ public void setAll(
+ SpsData spsData,
+ int nalRefIdc,
+ int sliceType,
+ int frameNum,
+ int picParameterSetId,
+ boolean fieldPicFlag,
+ boolean bottomFieldFlagPresent,
+ boolean bottomFieldFlag,
+ boolean idrPicFlag,
+ int idrPicId,
+ int picOrderCntLsb,
+ int deltaPicOrderCntBottom,
+ int deltaPicOrderCnt0,
+ int deltaPicOrderCnt1) {
this.spsData = spsData;
this.nalRefIdc = nalRefIdc;
this.sliceType = sliceType;
@@ -514,23 +542,26 @@ public final class H264Reader implements ElementaryStreamReader {
private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) {
// See ISO 14496-10 subsection 7.4.1.2.4.
- return isComplete && (!other.isComplete || frameNum != other.frameNum
- || picParameterSetId != other.picParameterSetId || fieldPicFlag != other.fieldPicFlag
- || (bottomFieldFlagPresent && other.bottomFieldFlagPresent
- && bottomFieldFlag != other.bottomFieldFlag)
- || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
- || (spsData.picOrderCountType == 0 && other.spsData.picOrderCountType == 0
- && (picOrderCntLsb != other.picOrderCntLsb
- || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))
- || (spsData.picOrderCountType == 1 && other.spsData.picOrderCountType == 1
- && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0
- || deltaPicOrderCnt1 != other.deltaPicOrderCnt1))
- || idrPicFlag != other.idrPicFlag
- || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId));
+ return isComplete
+ && (!other.isComplete
+ || frameNum != other.frameNum
+ || picParameterSetId != other.picParameterSetId
+ || fieldPicFlag != other.fieldPicFlag
+ || (bottomFieldFlagPresent
+ && other.bottomFieldFlagPresent
+ && bottomFieldFlag != other.bottomFieldFlag)
+ || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
+ || (spsData.picOrderCountType == 0
+ && other.spsData.picOrderCountType == 0
+ && (picOrderCntLsb != other.picOrderCntLsb
+ || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))
+ || (spsData.picOrderCountType == 1
+ && other.spsData.picOrderCountType == 1
+ && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0
+ || deltaPicOrderCnt1 != other.deltaPicOrderCnt1))
+ || idrPicFlag != other.idrPicFlag
+ || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId));
}
-
}
-
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java
index 13d679c47c..88bde53746 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java
@@ -104,7 +104,8 @@ public final class H265Reader implements ElementaryStreamReader {
}
@Override
- public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ // TODO (Internal b/32267012): Consider using random access indicator.
this.pesTimeUs = pesTimeUs;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java
index 0f0f2ad981..f936fb9e43 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
+import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
+
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@@ -63,8 +65,8 @@ public final class Id3Reader implements ElementaryStreamReader {
}
@Override
- public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
- if (!dataAlignmentIndicator) {
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
+ if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
return;
}
writingSample = true;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java
index f401a6e736..2a633c191d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java
@@ -93,7 +93,7 @@ public final class LatmReader implements ElementaryStreamReader {
}
@Override
- public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java
index effa7d7c96..393e297818 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java
@@ -83,7 +83,7 @@ public final class MpegAudioReader implements ElementaryStreamReader {
}
@Override
- public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+ public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java
index 91cd548367..ff755f4ece 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java
@@ -78,9 +78,8 @@ public final class PesReader implements TsPayloadReader {
}
@Override
- public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator)
- throws ParserException {
- if (payloadUnitStartIndicator) {
+ public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException {
+ if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) {
switch (state) {
case STATE_FINDING_HEADER:
case STATE_READING_HEADER:
@@ -122,7 +121,8 @@ public final class PesReader implements TsPayloadReader {
if (continueRead(data, pesScratch.data, readLength)
&& continueRead(data, null, extendedHeaderLength)) {
parseHeaderExtension();
- reader.packetStarted(timeUs, dataAlignmentIndicator);
+ flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;
+ reader.packetStarted(timeUs, flags);
setState(STATE_READING_BODY);
}
break;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
index c7a082aeac..f453a9cc43 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
@@ -343,7 +343,7 @@ public final class PsExtractor implements Extractor {
data.readBytes(pesScratch.data, 0, extendedHeaderLength);
pesScratch.setPosition(0);
parseHeaderExtension();
- pesPayloadReader.packetStarted(timeUs, true);
+ pesPayloadReader.packetStarted(timeUs, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR);
pesPayloadReader.consume(data);
// We always have complete PES packets with program stream.
pesPayloadReader.packetFinished();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java
index d217cfcb7a..101a1f74d9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java
@@ -57,7 +57,8 @@ public final class SectionReader implements TsPayloadReader {
}
@Override
- public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
+ public void consume(ParsableByteArray data, @Flags int flags) {
+ boolean payloadUnitStartIndicator = (flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0;
int payloadStartPosition = C.POSITION_UNSET;
if (payloadUnitStartIndicator) {
int payloadStartOffset = data.readUnsignedByte();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
index f47a481d7e..d91842423d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
+import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR;
+
import android.support.annotation.IntDef;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -279,6 +281,8 @@ public final class TsExtractor implements Extractor {
return RESULT_CONTINUE;
}
+ @TsPayloadReader.Flags int packetHeaderFlags = 0;
+
// Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
int tsPacketHeader = tsPacketBuffer.readInt();
if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator
@@ -286,7 +290,7 @@ public final class TsExtractor implements Extractor {
tsPacketBuffer.setPosition(endOfPacket);
return RESULT_CONTINUE;
}
- boolean payloadUnitStartIndicator = (tsPacketHeader & 0x400000) != 0;
+ packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0;
// Ignoring transport_priority (tsPacketHeader & 0x200000)
int pid = (tsPacketHeader & 0x1FFF00) >> 8;
// Ignoring transport_scrambling_control (tsPacketHeader & 0xC0)
@@ -317,14 +321,20 @@ public final class TsExtractor implements Extractor {
// Skip the adaptation field.
if (adaptationFieldExists) {
int adaptationFieldLength = tsPacketBuffer.readUnsignedByte();
- tsPacketBuffer.skipBytes(adaptationFieldLength);
+ int adaptationFieldFlags = tsPacketBuffer.readUnsignedByte();
+
+ packetHeaderFlags |=
+ (adaptationFieldFlags & 0x40) != 0 // random_access_indicator.
+ ? TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR
+ : 0;
+ tsPacketBuffer.skipBytes(adaptationFieldLength - 1 /* flags */);
}
// Read the payload.
boolean wereTracksEnded = tracksEnded;
if (shouldConsumePacketPayload(pid)) {
tsPacketBuffer.setLimit(endOfPacket);
- payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator);
+ payloadReader.consume(tsPacketBuffer, packetHeaderFlags);
tsPacketBuffer.setLimit(limit);
}
if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
index 2ea25bb2e0..a034b05696 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
@@ -15,12 +15,16 @@
*/
package com.google.android.exoplayer2.extractor.ts;
+import android.support.annotation.IntDef;
import android.util.SparseArray;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.List;
@@ -174,6 +178,29 @@ public interface TsPayloadReader {
}
+ /**
+ * Contextual flags indicating the presence of indicators in the TS packet or PES packet headers.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ FLAG_PAYLOAD_UNIT_START_INDICATOR,
+ FLAG_RANDOM_ACCESS_INDICATOR,
+ FLAG_DATA_ALIGNMENT_INDICATOR
+ })
+ @interface Flags {}
+
+ /** Indicates the presence of the payload_unit_start_indicator in the TS packet header. */
+ int FLAG_PAYLOAD_UNIT_START_INDICATOR = 1;
+ /**
+ * Indicates the presence of the random_access_indicator in the TS packet header adaptation field.
+ */
+ int FLAG_RANDOM_ACCESS_INDICATOR = 1 << 1;
+ /** Indicates the presence of the data_alignment_indicator in the PES header. */
+ int FLAG_DATA_ALIGNMENT_INDICATOR = 1 << 2;
+
/**
* Initializes the payload reader.
*
@@ -187,10 +214,10 @@ public interface TsPayloadReader {
/**
* Notifies the reader that a seek has occurred.
- *
- * Following a call to this method, the data passed to the next invocation of
- * {@link #consume(ParsableByteArray, boolean)} will not be a continuation of the data that was
- * previously passed. Hence the reader should reset any internal state.
+ *
+ *
Following a call to this method, the data passed to the next invocation of {@link #consume}
+ * will not be a continuation of the data that was previously passed. Hence the reader should
+ * reset any internal state.
*/
void seek();
@@ -198,9 +225,8 @@ public interface TsPayloadReader {
* Consumes the payload of a TS packet.
*
* @param data The TS packet. The position will be set to the start of the payload.
- * @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet.
+ * @param flags See {@link Flags}.
* @throws ParserException If the payload could not be parsed.
*/
- void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) throws ParserException;
-
+ void consume(ParsableByteArray data, @Flags int flags) throws ParserException;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
index 32f6bd5409..107ab9efd8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
@@ -248,9 +248,15 @@ public final class MediaCodecInfo {
// If we don't know any better, we assume that the profile and level are supported.
return true;
}
+ int profile = codecProfileAndLevel.first;
+ int level = codecProfileAndLevel.second;
+ if (!isVideo && profile != CodecProfileLevel.AACObjectXHE) {
+ // Some devices/builds underreport audio capabilities, so assume support except for xHE-AAC
+ // which may not be widely supported. See https://github.com/google/ExoPlayer/issues/5145.
+ return true;
+ }
for (CodecProfileLevel capabilities : getProfileLevels()) {
- if (capabilities.profile == codecProfileAndLevel.first
- && capabilities.level >= codecProfileAndLevel.second) {
+ if (capabilities.profile == profile && capabilities.level >= level) {
return true;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
index 2d936afc2a..35f5c14f3f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -20,6 +20,7 @@ import android.media.MediaCodec;
import android.media.MediaCodec.CodecException;
import android.media.MediaCodec.CryptoException;
import android.media.MediaCrypto;
+import android.media.MediaCryptoException;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.Looper;
@@ -239,14 +240,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Documented
@Retention(RetentionPolicy.SOURCE)
- @IntDef({DRAIN_ACTION_NONE, DRAIN_ACTION_FLUSH, DRAIN_ACTION_REINITIALIZE})
+ @IntDef({
+ DRAIN_ACTION_NONE,
+ DRAIN_ACTION_FLUSH,
+ DRAIN_ACTION_UPDATE_DRM_SESSION,
+ DRAIN_ACTION_REINITIALIZE
+ })
private @interface DrainAction {}
/** No special action should be taken. */
private static final int DRAIN_ACTION_NONE = 0;
/** The codec should be flushed. */
private static final int DRAIN_ACTION_FLUSH = 1;
- /** The codec should be re-initialized. */
- private static final int DRAIN_ACTION_REINITIALIZE = 2;
+ /** The codec should be flushed and updated to use the pending DRM session. */
+ private static final int DRAIN_ACTION_UPDATE_DRM_SESSION = 2;
+ /** The codec should be reinitialized. */
+ private static final int DRAIN_ACTION_REINITIALIZE = 3;
@Documented
@Retention(RetentionPolicy.SOURCE)
@@ -287,13 +295,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private final DecoderInputBuffer flagsOnlyBuffer;
private final FormatHolder formatHolder;
private final TimedValueQueue formatQueue;
- private final List decodeOnlyPresentationTimestamps;
+ private final ArrayList decodeOnlyPresentationTimestamps;
private final MediaCodec.BufferInfo outputBufferInfo;
@Nullable private Format inputFormat;
private Format outputFormat;
- private DrmSession drmSession;
- private DrmSession pendingDrmSession;
+ @Nullable private DrmSession codecDrmSession;
+ @Nullable private DrmSession sourceDrmSession;
+ @Nullable private MediaCrypto mediaCrypto;
+ private boolean mediaCryptoRequiresSecureDecoder;
private long renderTimeLimitMs;
private float rendererOperatingRate;
@Nullable private MediaCodec codec;
@@ -457,29 +467,36 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return;
}
- drmSession = pendingDrmSession;
+ setCodecDrmSession(sourceDrmSession);
+
String mimeType = inputFormat.sampleMimeType;
- MediaCrypto wrappedMediaCrypto = null;
- boolean drmSessionRequiresSecureDecoder = false;
- if (drmSession != null) {
- FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto();
+ if (codecDrmSession != null) {
if (mediaCrypto == null) {
- DrmSessionException drmError = drmSession.getError();
- if (drmError != null) {
- // Continue for now. We may be able to avoid failure if the session recovers, or if a new
- // input format causes the session to be replaced before it's used.
+ FrameworkMediaCrypto sessionMediaCrypto = codecDrmSession.getMediaCrypto();
+ if (sessionMediaCrypto == null) {
+ DrmSessionException drmError = codecDrmSession.getError();
+ if (drmError != null) {
+ // Continue for now. We may be able to avoid failure if the session recovers, or if a
+ // new input format causes the session to be replaced before it's used.
+ } else {
+ // The drm session isn't open yet.
+ return;
+ }
} else {
- // The drm session isn't open yet.
- return;
+ try {
+ mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId);
+ } catch (MediaCryptoException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ mediaCryptoRequiresSecureDecoder =
+ !sessionMediaCrypto.forceAllowInsecureDecoderComponents
+ && mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
- } else {
- wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
- drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
- @DrmSession.State int drmSessionState = drmSession.getState();
+ @DrmSession.State int drmSessionState = codecDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
} else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
// Wait for keys.
return;
@@ -488,7 +505,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
try {
- maybeInitCodecWithFallback(wrappedMediaCrypto, drmSessionRequiresSecureDecoder);
+ maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder);
} catch (DecoderInitializationException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
@@ -537,7 +554,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
inputStreamEnded = false;
outputStreamEnded = false;
- flushOrReinitCodec();
+ flushOrReinitializeCodec();
formatQueue.clear();
}
@@ -552,7 +569,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Override
protected void onDisabled() {
inputFormat = null;
- if (drmSession != null || pendingDrmSession != null) {
+ if (sourceDrmSession != null || codecDrmSession != null) {
// TODO: Do something better with this case.
onReset();
} else {
@@ -565,51 +582,40 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
try {
releaseCodec();
} finally {
- try {
- if (drmSession != null) {
- drmSessionManager.releaseSession(drmSession);
- }
- } finally {
- try {
- if (pendingDrmSession != null && pendingDrmSession != drmSession) {
- drmSessionManager.releaseSession(pendingDrmSession);
- }
- } finally {
- drmSession = null;
- pendingDrmSession = null;
- }
- }
+ setSourceDrmSession(null);
}
}
protected void releaseCodec() {
availableCodecInfos = null;
- if (codec != null) {
- codecInfo = null;
- codecFormat = null;
- resetInputBuffer();
- resetOutputBuffer();
- resetCodecBuffers();
- waitingForKeys = false;
- codecHotswapDeadlineMs = C.TIME_UNSET;
- decodeOnlyPresentationTimestamps.clear();
- decoderCounters.decoderReleaseCount++;
- try {
- codec.stop();
- } finally {
+ codecInfo = null;
+ codecFormat = null;
+ resetInputBuffer();
+ resetOutputBuffer();
+ resetCodecBuffers();
+ waitingForKeys = false;
+ codecHotswapDeadlineMs = C.TIME_UNSET;
+ decodeOnlyPresentationTimestamps.clear();
+ try {
+ if (codec != null) {
+ decoderCounters.decoderReleaseCount++;
try {
- codec.release();
+ codec.stop();
} finally {
- codec = null;
- if (drmSession != null && pendingDrmSession != drmSession) {
- try {
- drmSessionManager.releaseSession(drmSession);
- } finally {
- drmSession = null;
- }
- }
+ codec.release();
}
}
+ } finally {
+ codec = null;
+ try {
+ if (mediaCrypto != null) {
+ mediaCrypto.release();
+ }
+ } finally {
+ mediaCrypto = null;
+ mediaCryptoRequiresSecureDecoder = false;
+ setCodecDrmSession(null);
+ }
}
}
@@ -680,12 +686,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link
* #maybeInitCodec()} if the codec needs to be re-instantiated.
*
+ * @return Whether the codec was released and reinitialized, rather than being flushed.
* @throws ExoPlaybackException If an error occurs re-instantiating the codec.
*/
- protected final void flushOrReinitCodec() throws ExoPlaybackException {
- if (flushOrReleaseCodec()) {
+ protected final boolean flushOrReinitializeCodec() throws ExoPlaybackException {
+ boolean released = flushOrReleaseCodec();
+ if (released) {
maybeInitCodec();
}
+ return released;
}
/**
@@ -729,18 +738,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
private void maybeInitCodecWithFallback(
- MediaCrypto crypto, boolean drmSessionRequiresSecureDecoder)
+ MediaCrypto crypto, boolean mediaCryptoRequiresSecureDecoder)
throws DecoderInitializationException {
if (availableCodecInfos == null) {
try {
availableCodecInfos =
- new ArrayDeque<>(getAvailableCodecInfos(drmSessionRequiresSecureDecoder));
+ new ArrayDeque<>(getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder));
preferredDecoderInitializationException = null;
} catch (DecoderQueryException e) {
throw new DecoderInitializationException(
inputFormat,
e,
- drmSessionRequiresSecureDecoder,
+ mediaCryptoRequiresSecureDecoder,
DecoderInitializationException.DECODER_QUERY_ERROR);
}
}
@@ -749,7 +758,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
throw new DecoderInitializationException(
inputFormat,
/* cause= */ null,
- drmSessionRequiresSecureDecoder,
+ mediaCryptoRequiresSecureDecoder,
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR);
}
@@ -768,7 +777,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
availableCodecInfos.removeFirst();
DecoderInitializationException exception =
new DecoderInitializationException(
- inputFormat, e, drmSessionRequiresSecureDecoder, codecInfo.name);
+ inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo.name);
if (preferredDecoderInitializationException == null) {
preferredDecoderInitializationException = exception;
} else {
@@ -784,11 +793,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
availableCodecInfos = null;
}
- private List getAvailableCodecInfos(boolean drmSessionRequiresSecureDecoder)
+ private List getAvailableCodecInfos(boolean mediaCryptoRequiresSecureDecoder)
throws DecoderQueryException {
List codecInfos =
- getDecoderInfos(mediaCodecSelector, inputFormat, drmSessionRequiresSecureDecoder);
- if (codecInfos.isEmpty() && drmSessionRequiresSecureDecoder) {
+ getDecoderInfos(mediaCodecSelector, inputFormat, mediaCryptoRequiresSecureDecoder);
+ if (codecInfos.isEmpty() && mediaCryptoRequiresSecureDecoder) {
// The drm session indicates that a secure decoder is required, but the device does not
// have one. Assuming that supportsFormat indicated support for the media being played, we
// know that it does not require a secure output path. Most CDM implementations allow
@@ -928,6 +937,24 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputBuffer = null;
}
+ private void setSourceDrmSession(@Nullable DrmSession session) {
+ DrmSession previous = sourceDrmSession;
+ sourceDrmSession = session;
+ releaseDrmSessionIfUnused(previous);
+ }
+
+ private void setCodecDrmSession(@Nullable DrmSession session) {
+ DrmSession previous = codecDrmSession;
+ codecDrmSession = session;
+ releaseDrmSessionIfUnused(previous);
+ }
+
+ private void releaseDrmSessionIfUnused(@Nullable DrmSession session) {
+ if (session != null && session != sourceDrmSession && session != codecDrmSession) {
+ drmSessionManager.releaseSession(session);
+ }
+ }
+
/**
* @return Whether it may be possible to feed more input data.
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
@@ -1082,12 +1109,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
+ if (codecDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
- @DrmSession.State int drmSessionState = drmSession.getState();
+ @DrmSession.State int drmSessionState = codecDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
}
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
@@ -1126,13 +1153,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
}
- pendingDrmSession =
+ DrmSession session =
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
- if (pendingDrmSession == drmSession) {
- drmSessionManager.releaseSession(pendingDrmSession);
+ if (session == sourceDrmSession || session == codecDrmSession) {
+ // We already had this session. The manager must be reference counting, so release it once
+ // to get the count attributed to this renderer back down to 1.
+ drmSessionManager.releaseSession(session);
}
+ setSourceDrmSession(session);
} else {
- pendingDrmSession = null;
+ setSourceDrmSession(null);
}
}
@@ -1143,40 +1173,58 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
// We have an existing codec that we may need to reconfigure or re-initialize. If the existing
// codec instance is being kept then its operating rate may need to be updated.
- if (pendingDrmSession != drmSession) {
+
+ if ((sourceDrmSession == null && codecDrmSession != null)
+ || (sourceDrmSession != null && codecDrmSession == null)
+ || (sourceDrmSession != null && !codecInfo.secure)
+ || (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) {
+ // We might need to switch between the clear and protected output paths, or we're using DRM
+ // prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM
+ // session.
drainAndReinitializeCodec();
- } else {
- switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {
- case KEEP_CODEC_RESULT_NO:
- drainAndReinitializeCodec();
- break;
- case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
+ return;
+ }
+
+ switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {
+ case KEEP_CODEC_RESULT_NO:
+ drainAndReinitializeCodec();
+ break;
+ case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
+ codecFormat = newFormat;
+ updateCodecOperatingRate();
+ if (sourceDrmSession != codecDrmSession) {
+ drainAndUpdateCodecDrmSession();
+ } else {
drainAndFlushCodec();
+ }
+ break;
+ case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION:
+ if (codecNeedsReconfigureWorkaround) {
+ drainAndReinitializeCodec();
+ } else {
+ codecReconfigured = true;
+ codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+ codecNeedsAdaptationWorkaroundBuffer =
+ codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS
+ || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION
+ && newFormat.width == codecFormat.width
+ && newFormat.height == codecFormat.height);
codecFormat = newFormat;
updateCodecOperatingRate();
- break;
- case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION:
- if (codecNeedsReconfigureWorkaround) {
- drainAndReinitializeCodec();
- } else {
- codecReconfigured = true;
- codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
- codecNeedsAdaptationWorkaroundBuffer =
- codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS
- || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION
- && newFormat.width == codecFormat.width
- && newFormat.height == codecFormat.height);
- codecFormat = newFormat;
- updateCodecOperatingRate();
+ if (sourceDrmSession != codecDrmSession) {
+ drainAndUpdateCodecDrmSession();
}
- break;
- case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:
- codecFormat = newFormat;
- updateCodecOperatingRate();
- break;
- default:
- throw new IllegalStateException(); // Never happens.
- }
+ }
+ break;
+ case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:
+ codecFormat = newFormat;
+ updateCodecOperatingRate();
+ if (sourceDrmSession != codecDrmSession) {
+ drainAndUpdateCodecDrmSession();
+ }
+ break;
+ default:
+ throw new IllegalStateException(); // Never happens.
}
}
@@ -1311,6 +1359,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
}
+ /**
+ * Starts draining the codec to update its DRM session. The update may occur immediately if no
+ * buffers have been queued to the codec.
+ *
+ * @throws ExoPlaybackException If an error occurs updating the codec's DRM session.
+ */
+ private void drainAndUpdateCodecDrmSession() throws ExoPlaybackException {
+ if (Util.SDK_INT < 23) {
+ // The codec needs to be re-initialized to switch to the source DRM session.
+ drainAndReinitializeCodec();
+ return;
+ }
+ if (codecReceivedBuffers) {
+ codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM;
+ codecDrainAction = DRAIN_ACTION_UPDATE_DRM_SESSION;
+ } else {
+ // Nothing has been queued to the decoder, so we can do the update immediately.
+ updateDrmSessionOrReinitializeCodecV23();
+ }
+ }
+
/**
* Starts draining the codec for re-initialization. Re-initialization may occur immediately if no
* buffers have been queued to the codec.
@@ -1323,8 +1392,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecDrainAction = DRAIN_ACTION_REINITIALIZE;
} else {
// Nothing has been queued to the decoder, so we can re-initialize immediately.
- releaseCodec();
- maybeInitCodec();
+ reinitializeCodec();
}
}
@@ -1528,11 +1596,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private void processEndOfStream() throws ExoPlaybackException {
switch (codecDrainAction) {
case DRAIN_ACTION_REINITIALIZE:
- releaseCodec();
- maybeInitCodec();
+ reinitializeCodec();
+ break;
+ case DRAIN_ACTION_UPDATE_DRM_SESSION:
+ updateDrmSessionOrReinitializeCodecV23();
break;
case DRAIN_ACTION_FLUSH:
- flushOrReinitCodec();
+ flushOrReinitializeCodec();
break;
case DRAIN_ACTION_NONE:
default:
@@ -1542,6 +1612,41 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
}
+ private void reinitializeCodec() throws ExoPlaybackException {
+ releaseCodec();
+ maybeInitCodec();
+ }
+
+ @TargetApi(23)
+ private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackException {
+ FrameworkMediaCrypto sessionMediaCrypto = sourceDrmSession.getMediaCrypto();
+ if (sessionMediaCrypto == null) {
+ // We'd only expect this to happen if the CDM from which the pending session is obtained needs
+ // provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme
+ // to another, where the new CDM hasn't been used before and needs provisioning). It would be
+ // possible to handle this case more efficiently (i.e. with a new renderer state that waits
+ // for provisioning to finish and then calls mediaCrypto.setMediaDrmSession), but the extra
+ // complexity is not warranted given how unlikely the case is to occur.
+ reinitializeCodec();
+ return;
+ }
+
+ if (flushOrReinitializeCodec()) {
+ // The codec was reinitialized. The new codec will be using the new DRM session, so there's
+ // nothing more to do.
+ return;
+ }
+
+ try {
+ mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId);
+ } catch (MediaCryptoException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ setCodecDrmSession(sourceDrmSession);
+ codecDrainState = DRAIN_STATE_NONE;
+ codecDrainAction = DRAIN_ACTION_NONE;
+ }
+
private boolean shouldSkipOutputBuffer(long presentationTimeUs) {
// We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would
// box presentationTimeUs, creating a Long object that would need to be garbage collected.
@@ -1693,7 +1798,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/
private static boolean codecNeedsEosFlushWorkaround(String name) {
return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name))
- || (Util.SDK_INT <= 19 && "hb2000".equals(Util.DEVICE)
+ || (Util.SDK_INT <= 19
+ && ("hb2000".equals(Util.DEVICE) || "stvm8".equals(Util.DEVICE))
&& ("OMX.amlogic.avc.decoder.awesome".equals(name)
|| "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
index 893601a859..9ae50179c3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
@@ -318,7 +318,23 @@ public final class MediaCodecUtil {
}
// Work around https://github.com/google/ExoPlayer/issues/4519.
- if ("OMX.SEC.mp3.dec".equals(name) && "SM-T530".equals(Util.MODEL)) {
+ if ("OMX.SEC.mp3.dec".equals(name)
+ && (Util.MODEL.startsWith("GT-I9152")
+ || Util.MODEL.startsWith("GT-I9515")
+ || Util.MODEL.startsWith("GT-P5220")
+ || Util.MODEL.startsWith("GT-S7580")
+ || Util.MODEL.startsWith("SM-G350")
+ || Util.MODEL.startsWith("SM-G386")
+ || Util.MODEL.startsWith("SM-T231")
+ || Util.MODEL.startsWith("SM-T530")
+ || Util.MODEL.startsWith("SCH-I535")
+ || Util.MODEL.startsWith("SPH-L710"))) {
+ return false;
+ }
+ if ("OMX.brcm.audio.mp3.decoder".equals(name)
+ && (Util.MODEL.startsWith("GT-I9152")
+ || Util.MODEL.startsWith("GT-S7580")
+ || Util.MODEL.startsWith("SM-G350"))) {
return false;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java
index a2ad7fe2ce..fbed096aab 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java
@@ -18,8 +18,10 @@ package com.google.android.exoplayer2.metadata;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* A collection of metadata entries.
@@ -76,6 +78,18 @@ public final class Metadata implements Parcelable {
return entries[index];
}
+ /**
+ * Returns a copy of this metadata with the specified entries appended.
+ *
+ * @param entriesToAppend The entries to append.
+ * @return The metadata instance with the appended entries.
+ */
+ public Metadata copyWithAppendedEntries(Entry... entriesToAppend) {
+ @NullableType Entry[] merged = Arrays.copyOf(entries, entries.length + entriesToAppend.length);
+ System.arraycopy(entriesToAppend, 0, merged, entries.length, entriesToAppend.length);
+ return new Metadata(Util.castNonNullTypeArray(merged));
+ }
+
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java
index 028a8eb893..ae4b7db5c9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
+import com.google.android.exoplayer2.metadata.icy.IcyDecoder;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder;
import com.google.android.exoplayer2.util.MimeTypes;
@@ -46,38 +47,43 @@ public interface MetadataDecoderFactory {
/**
* Default {@link MetadataDecoder} implementation.
- *
- * The formats supported by this factory are:
+ *
+ *
The formats supported by this factory are:
+ *
*
- * - ID3 ({@link Id3Decoder})
- * - EMSG ({@link EventMessageDecoder})
- * - SCTE-35 ({@link SpliceInfoDecoder})
+ * - ID3 ({@link Id3Decoder})
+ *
- EMSG ({@link EventMessageDecoder})
+ *
- SCTE-35 ({@link SpliceInfoDecoder})
+ *
- ICY ({@link IcyDecoder})
*
*/
- 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");
+ }
+ }
+ };
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java
new file mode 100644
index 0000000000..1eac663956
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.metadata.icy;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.MetadataDecoder;
+import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Decodes ICY stream information. */
+public final class IcyDecoder implements MetadataDecoder {
+
+ private static final String TAG = "IcyDecoder";
+
+ private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';");
+ private static final String STREAM_KEY_NAME = "streamtitle";
+ private static final String STREAM_KEY_URL = "streamurl";
+
+ @Override
+ @Nullable
+ @SuppressWarnings("ByteBufferBackingArray")
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = inputBuffer.data;
+ byte[] data = buffer.array();
+ int length = buffer.limit();
+ return decode(Util.fromUtf8Bytes(data, 0, length));
+ }
+
+ @Nullable
+ @VisibleForTesting
+ /* package */ Metadata decode(String metadata) {
+ String name = null;
+ String url = null;
+ int index = 0;
+ Matcher matcher = METADATA_ELEMENT.matcher(metadata);
+ while (matcher.find(index)) {
+ String key = Util.toLowerInvariant(matcher.group(1));
+ String value = matcher.group(2);
+ switch (key) {
+ case STREAM_KEY_NAME:
+ name = value;
+ break;
+ case STREAM_KEY_URL:
+ url = value;
+ break;
+ default:
+ Log.w(TAG, "Unrecognized ICY tag: " + name);
+ break;
+ }
+ index = matcher.end();
+ }
+ return (name != null || url != null) ? new Metadata(new IcyInfo(name, url)) : null;
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java
new file mode 100644
index 0000000000..cd8c5b17d2
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.metadata.icy;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.util.Util;
+import java.util.List;
+import java.util.Map;
+
+/** ICY headers. */
+public final class IcyHeaders implements Metadata.Entry {
+
+ public static final String REQUEST_HEADER_ENABLE_METADATA_NAME = "Icy-MetaData";
+ public static final String REQUEST_HEADER_ENABLE_METADATA_VALUE = "1";
+
+ private static final String TAG = "IcyHeaders";
+
+ private static final String RESPONSE_HEADER_BITRATE = "icy-br";
+ private static final String RESPONSE_HEADER_GENRE = "icy-genre";
+ private static final String RESPONSE_HEADER_NAME = "icy-name";
+ private static final String RESPONSE_HEADER_URL = "icy-url";
+ private static final String RESPONSE_HEADER_PUB = "icy-pub";
+ private static final String RESPONSE_HEADER_METADATA_INTERVAL = "icy-metaint";
+
+ /**
+ * Parses {@link IcyHeaders} from response headers.
+ *
+ * @param responseHeaders The response headers.
+ * @return The parsed {@link IcyHeaders}, or {@code null} if no ICY headers were present.
+ */
+ @Nullable
+ public static IcyHeaders parse(Map> 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 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 CREATOR =
+ new Parcelable.Creator() {
+
+ @Override
+ public IcyHeaders createFromParcel(Parcel in) {
+ return new IcyHeaders(in);
+ }
+
+ @Override
+ public IcyHeaders[] newArray(int size) {
+ return new IcyHeaders[size];
+ }
+ };
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java
new file mode 100644
index 0000000000..a9671bb68d
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.metadata.icy;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.Util;
+
+/** ICY in-stream information. */
+public final class IcyInfo implements Metadata.Entry {
+
+ /** The stream title if present, or {@code null}. */
+ @Nullable public final String title;
+ /** The stream title if present, or {@code null}. */
+ @Nullable public final String url;
+
+ /**
+ * @param title See {@link #title}.
+ * @param url See {@link #url}.
+ */
+ public IcyInfo(@Nullable String title, @Nullable String url) {
+ this.title = title;
+ this.url = url;
+ }
+
+ /* package */ IcyInfo(Parcel in) {
+ title = in.readString();
+ url = in.readString();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ IcyInfo other = (IcyInfo) obj;
+ return Util.areEqual(title, other.title) && Util.areEqual(url, other.url);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (title != null ? title.hashCode() : 0);
+ result = 31 * result + (url != null ? url.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ICY: title=\"" + title + "\", url=\"" + url + "\"";
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(title);
+ dest.writeString(url);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+
+ @Override
+ public IcyInfo createFromParcel(Parcel in) {
+ return new IcyInfo(in);
+ }
+
+ @Override
+ public IcyInfo[] newArray(int size) {
+ return new IcyInfo[size];
+ }
+ };
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java
new file mode 100644
index 0000000000..28a5abafb9
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import com.google.android.exoplayer2.database.DatabaseProvider;
+import com.google.android.exoplayer2.database.VersionTable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * A {@link DownloadIndex} which uses SQLite to persist {@link DownloadState}s.
+ *
+ * Database access may take a long time, do not call methods of this class from
+ * the application main thread.
+ */
+public final class DefaultDownloadIndex implements DownloadIndex {
+
+ @VisibleForTesting
+ /* package */ static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Downloads";
+
+ @VisibleForTesting /* package */ static final int TABLE_VERSION = 1;
+
+ private final DatabaseProvider databaseProvider;
+ @Nullable private DownloadsTable downloadTable;
+
+ /**
+ * Creates a DefaultDownloadIndex which stores the {@link DownloadState}s on a SQLite database
+ * provided by {@code databaseProvider}.
+ *
+ * @param databaseProvider A DatabaseProvider which provides the database which will be used to
+ * store DownloadStatus table.
+ */
+ public DefaultDownloadIndex(DatabaseProvider databaseProvider) {
+ this.databaseProvider = databaseProvider;
+ }
+
+ @Override
+ @Nullable
+ public DownloadState getDownloadState(String id) {
+ return getDownloadTable().get(id);
+ }
+
+ @Override
+ public DownloadStateCursor getDownloadStates(@DownloadState.State int... states) {
+ return getDownloadTable().get(states);
+ }
+
+ @Override
+ public void putDownloadState(DownloadState downloadState) {
+ getDownloadTable().replace(downloadState);
+ }
+
+ @Override
+ public void removeDownloadState(String id) {
+ getDownloadTable().delete(id);
+ }
+
+ private DownloadsTable getDownloadTable() {
+ if (downloadTable == null) {
+ downloadTable = new DownloadsTable(databaseProvider);
+ }
+ return downloadTable;
+ }
+
+ private static final class DownloadStateCursorImpl implements DownloadStateCursor {
+
+ private final Cursor cursor;
+
+ private DownloadStateCursorImpl(Cursor cursor) {
+ this.cursor = cursor;
+ }
+
+ @Override
+ public DownloadState getDownloadState() {
+ return DownloadsTable.getDownloadState(cursor);
+ }
+
+ @Override
+ public int getCount() {
+ return cursor.getCount();
+ }
+
+ @Override
+ public int getPosition() {
+ return cursor.getPosition();
+ }
+
+ @Override
+ public boolean moveToPosition(int position) {
+ return cursor.moveToPosition(position);
+ }
+
+ @Override
+ public void close() {
+ cursor.close();
+ }
+
+ @Override
+ public boolean isClosed() {
+ return cursor.isClosed();
+ }
+ }
+
+ private static final class DownloadsTable {
+
+ private static final String COLUMN_ID = "id";
+ private static final String COLUMN_TYPE = "title";
+ private static final String COLUMN_URI = "subtitle";
+ private static final String COLUMN_CACHE_KEY = "cache_key";
+ private static final String COLUMN_STATE = "state";
+ private static final String COLUMN_DOWNLOAD_PERCENTAGE = "download_percentage";
+ private static final String COLUMN_DOWNLOADED_BYTES = "downloaded_bytes";
+ private static final String COLUMN_TOTAL_BYTES = "total_bytes";
+ private static final String COLUMN_FAILURE_REASON = "failure_reason";
+ private static final String COLUMN_STOP_FLAGS = "stop_flags";
+ private static final String COLUMN_START_TIME_MS = "start_time_ms";
+ private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms";
+ private static final String COLUMN_STREAM_KEYS = "stream_keys";
+ private static final String COLUMN_CUSTOM_METADATA = "custom_metadata";
+
+ private static final int COLUMN_INDEX_ID = 0;
+ private static final int COLUMN_INDEX_TYPE = 1;
+ private static final int COLUMN_INDEX_URI = 2;
+ private static final int COLUMN_INDEX_CACHE_KEY = 3;
+ private static final int COLUMN_INDEX_STATE = 4;
+ private static final int COLUMN_INDEX_DOWNLOAD_PERCENTAGE = 5;
+ private static final int COLUMN_INDEX_DOWNLOADED_BYTES = 6;
+ private static final int COLUMN_INDEX_TOTAL_BYTES = 7;
+ private static final int COLUMN_INDEX_FAILURE_REASON = 8;
+ private static final int COLUMN_INDEX_STOP_FLAGS = 9;
+ private static final int COLUMN_INDEX_START_TIME_MS = 10;
+ private static final int COLUMN_INDEX_UPDATE_TIME_MS = 11;
+ private static final int COLUMN_INDEX_STREAM_KEYS = 12;
+ private static final int COLUMN_INDEX_CUSTOM_METADATA = 13;
+
+ private static final String COLUMN_SELECTION_ID = COLUMN_ID + " = ?";
+
+ private static final String[] COLUMNS =
+ new String[] {
+ COLUMN_ID,
+ COLUMN_TYPE,
+ COLUMN_URI,
+ COLUMN_CACHE_KEY,
+ COLUMN_STATE,
+ COLUMN_DOWNLOAD_PERCENTAGE,
+ COLUMN_DOWNLOADED_BYTES,
+ COLUMN_TOTAL_BYTES,
+ COLUMN_FAILURE_REASON,
+ COLUMN_STOP_FLAGS,
+ COLUMN_START_TIME_MS,
+ COLUMN_UPDATE_TIME_MS,
+ COLUMN_STREAM_KEYS,
+ COLUMN_CUSTOM_METADATA
+ };
+
+ private static final String SQL_DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS " + TABLE_NAME;
+ private static final String SQL_CREATE_TABLE =
+ "CREATE TABLE "
+ + TABLE_NAME
+ + " ("
+ + COLUMN_ID
+ + " TEXT PRIMARY KEY NOT NULL,"
+ + COLUMN_TYPE
+ + " TEXT NOT NULL,"
+ + COLUMN_URI
+ + " TEXT NOT NULL,"
+ + COLUMN_CACHE_KEY
+ + " TEXT,"
+ + COLUMN_STATE
+ + " INTEGER NOT NULL,"
+ + COLUMN_DOWNLOAD_PERCENTAGE
+ + " REAL NOT NULL,"
+ + COLUMN_DOWNLOADED_BYTES
+ + " INTEGER NOT NULL,"
+ + COLUMN_TOTAL_BYTES
+ + " INTEGER NOT NULL,"
+ + COLUMN_FAILURE_REASON
+ + " INTEGER NOT NULL,"
+ + COLUMN_STOP_FLAGS
+ + " INTEGER NOT NULL,"
+ + COLUMN_START_TIME_MS
+ + " INTEGER NOT NULL,"
+ + COLUMN_UPDATE_TIME_MS
+ + " INTEGER NOT NULL,"
+ + COLUMN_STREAM_KEYS
+ + " TEXT NOT NULL,"
+ + COLUMN_CUSTOM_METADATA
+ + " BLOB NOT NULL)";
+
+ private final DatabaseProvider databaseProvider;
+
+ public DownloadsTable(DatabaseProvider databaseProvider) {
+ this.databaseProvider = databaseProvider;
+ VersionTable versionTable = new VersionTable(databaseProvider);
+ int version = versionTable.getVersion(VersionTable.FEATURE_OFFLINE);
+ if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) {
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ writableDatabase.beginTransaction();
+ try {
+ writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS);
+ writableDatabase.execSQL(SQL_CREATE_TABLE);
+ versionTable.setVersion(VersionTable.FEATURE_OFFLINE, TABLE_VERSION);
+ writableDatabase.setTransactionSuccessful();
+ } finally {
+ writableDatabase.endTransaction();
+ }
+ } else if (version < TABLE_VERSION) {
+ // There is no previous version currently.
+ throw new IllegalStateException();
+ }
+ }
+
+ public void replace(DownloadState downloadState) {
+ ContentValues values = new ContentValues();
+ values.put(COLUMN_ID, downloadState.id);
+ values.put(COLUMN_TYPE, downloadState.type);
+ values.put(COLUMN_URI, downloadState.uri.toString());
+ values.put(COLUMN_CACHE_KEY, downloadState.cacheKey);
+ values.put(COLUMN_STATE, downloadState.state);
+ values.put(COLUMN_DOWNLOAD_PERCENTAGE, downloadState.downloadPercentage);
+ values.put(COLUMN_DOWNLOADED_BYTES, downloadState.downloadedBytes);
+ values.put(COLUMN_TOTAL_BYTES, downloadState.totalBytes);
+ values.put(COLUMN_FAILURE_REASON, downloadState.failureReason);
+ values.put(COLUMN_STOP_FLAGS, downloadState.stopFlags);
+ values.put(COLUMN_START_TIME_MS, downloadState.startTimeMs);
+ values.put(COLUMN_UPDATE_TIME_MS, downloadState.updateTimeMs);
+ values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(downloadState.streamKeys));
+ values.put(COLUMN_CUSTOM_METADATA, downloadState.customMetadata);
+ SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
+ writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values);
+ }
+
+ @Nullable
+ public DownloadState get(String id) {
+ String[] selectionArgs = {id};
+ try (Cursor cursor = query(COLUMN_SELECTION_ID, selectionArgs)) {
+ if (cursor.getCount() == 0) {
+ return null;
+ }
+ cursor.moveToNext();
+ DownloadState downloadState = getDownloadState(cursor);
+ Assertions.checkState(id.equals(downloadState.id));
+ return downloadState;
+ }
+ }
+
+ public DownloadStateCursor get(@DownloadState.State int... states) {
+ String selection = null;
+ if (states.length > 0) {
+ StringBuilder selectionBuilder = new StringBuilder();
+ selectionBuilder.append(COLUMN_STATE).append(" IN (");
+ for (int i = 0; i < states.length; i++) {
+ if (i > 0) {
+ selectionBuilder.append(',');
+ }
+ selectionBuilder.append(states[i]);
+ }
+ selectionBuilder.append(')');
+ selection = selectionBuilder.toString();
+ }
+ Cursor cursor = query(selection, /* selectionArgs= */ null);
+ return new DownloadStateCursorImpl(cursor);
+ }
+
+ public void delete(String id) {
+ String[] selectionArgs = {id};
+ databaseProvider.getWritableDatabase().delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs);
+ }
+
+ private Cursor query(@Nullable String selection, @Nullable String[] selectionArgs) {
+ String sortOrder = COLUMN_START_TIME_MS + " ASC";
+ return databaseProvider
+ .getReadableDatabase()
+ .query(
+ TABLE_NAME,
+ COLUMNS,
+ selection,
+ selectionArgs,
+ /* groupBy= */ null,
+ /* having= */ null,
+ sortOrder);
+ }
+
+ private static DownloadState getDownloadState(Cursor cursor) {
+ return new DownloadState(
+ cursor.getString(COLUMN_INDEX_ID),
+ cursor.getString(COLUMN_INDEX_TYPE),
+ Uri.parse(cursor.getString(COLUMN_INDEX_URI)),
+ cursor.getString(COLUMN_INDEX_CACHE_KEY),
+ cursor.getInt(COLUMN_INDEX_STATE),
+ cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE),
+ cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES),
+ cursor.getLong(COLUMN_INDEX_TOTAL_BYTES),
+ cursor.getInt(COLUMN_INDEX_FAILURE_REASON),
+ cursor.getInt(COLUMN_INDEX_STOP_FLAGS),
+ cursor.getLong(COLUMN_INDEX_START_TIME_MS),
+ cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS),
+ decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)),
+ cursor.getBlob(COLUMN_INDEX_CUSTOM_METADATA));
+ }
+
+ private static String encodeStreamKeys(StreamKey[] streamKeys) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (StreamKey streamKey : streamKeys) {
+ stringBuilder
+ .append(streamKey.periodIndex)
+ .append('.')
+ .append(streamKey.groupIndex)
+ .append('.')
+ .append(streamKey.trackIndex)
+ .append(',');
+ }
+ if (stringBuilder.length() > 0) {
+ stringBuilder.setLength(stringBuilder.length() - 1);
+ }
+ return stringBuilder.toString();
+ }
+
+ private static StreamKey[] decodeStreamKeys(String encodedStreamKeys) {
+ if (encodedStreamKeys.isEmpty()) {
+ return new StreamKey[0];
+ }
+ String[] streamKeysStrings = Util.split(encodedStreamKeys, ",");
+ int streamKeysCount = streamKeysStrings.length;
+ StreamKey[] streamKeys = new StreamKey[streamKeysCount];
+ for (int i = 0; i < streamKeysCount; i++) {
+ String[] indices = Util.split(streamKeysStrings[i], "\\.");
+ Assertions.checkState(indices.length == 3);
+ streamKeys[i] =
+ new StreamKey(
+ Integer.parseInt(indices[0]),
+ Integer.parseInt(indices[1]),
+ Integer.parseInt(indices[2]));
+ }
+ return streamKeys;
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
index 2c7b5069b9..40ea094b5e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java
@@ -77,7 +77,7 @@ public final class DownloadAction {
*
* @param type The type of the action.
* @param uri The URI of the media to be downloaded.
- * @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded.
+ * @param keys Keys of streams to be downloaded. If empty, all streams will be downloaded.
* @param customCacheKey A custom key for cache indexing, or null.
* @param data Optional custom data for this action. If {@code null} an empty array will be used.
*/
@@ -108,6 +108,8 @@ public final class DownloadAction {
/* data= */ null);
}
+ /** The unique content id. */
+ public final String id;
/** The type of the action. */
public final String type;
/** The uri being downloaded or removed. */
@@ -115,8 +117,8 @@ public final class DownloadAction {
/** Whether this is a remove action. If false, this is a download action. */
public final boolean isRemoveAction;
/**
- * Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if this action
- * is a remove action.
+ * Keys of streams to be downloaded. If empty, all streams will be downloaded. Empty if this
+ * action is a remove action.
*/
public final List keys;
/** A custom key for cache indexing, or null. */
@@ -128,8 +130,8 @@ public final class DownloadAction {
* @param type The type of the action.
* @param uri The uri being downloaded or removed.
* @param isRemoveAction Whether this is a remove action. If false, this is a download action.
- * @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if
- * this action is a remove action.
+ * @param keys Keys of streams to be downloaded. If empty, all streams will be downloaded. Empty
+ * if this action is a remove action.
* @param customCacheKey A custom key for cache indexing, or null.
* @param data Custom data for this action. Null if this action is a remove action.
*/
@@ -140,6 +142,7 @@ public final class DownloadAction {
List keys,
@Nullable String customCacheKey,
@Nullable byte[] data) {
+ this.id = customCacheKey != null ? customCacheKey : uri.toString();
this.type = type;
this.uri = uri;
this.isRemoveAction = isRemoveAction;
@@ -153,7 +156,7 @@ public final class DownloadAction {
ArrayList mutableKeys = new ArrayList<>(keys);
Collections.sort(mutableKeys);
this.keys = Collections.unmodifiableList(mutableKeys);
- this.data = data != null ? data : Util.EMPTY_BYTE_ARRAY;
+ this.data = data != null ? Arrays.copyOf(data, data.length) : Util.EMPTY_BYTE_ARRAY;
}
}
@@ -171,12 +174,10 @@ public final class DownloadAction {
/** Returns whether this is an action for the same media as the {@code other}. */
public boolean isSameMedia(DownloadAction other) {
- return customCacheKey == null
- ? other.customCacheKey == null && uri.equals(other.uri)
- : customCacheKey.equals(other.customCacheKey);
+ return id.equals(other.id);
}
- /** Returns keys of tracks to be downloaded. */
+ /** Returns keys of streams to be downloaded. */
public List getKeys() {
return keys;
}
@@ -187,7 +188,8 @@ public final class DownloadAction {
return false;
}
DownloadAction that = (DownloadAction) o;
- return type.equals(that.type)
+ return id.equals(that.id)
+ && type.equals(that.type)
&& uri.equals(that.uri)
&& isRemoveAction == that.isRemoveAction
&& keys.equals(that.keys)
@@ -198,6 +200,7 @@ public final class DownloadAction {
@Override
public final int hashCode() {
int result = type.hashCode();
+ result = 31 * result + id.hashCode();
result = 31 * result + uri.hashCode();
result = 31 * result + (isRemoveAction ? 1 : 0);
result = 31 * result + keys.hashCode();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadActionUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadActionUtil.java
new file mode 100644
index 0000000000..f722f9b59b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadActionUtil.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashSet;
+
+/** {@link DownloadAction} related utility methods. */
+public class DownloadActionUtil {
+
+ private DownloadActionUtil() {}
+
+ /**
+ * Merge {@link DownloadAction}s in {@code actionQueue} to minimum number of actions.
+ *
+ * All actions must have the same type and must be for the same media.
+ *
+ * @param actionQueue Queue of actions. Must not be empty.
+ * @return The first action in the queue.
+ */
+ public static DownloadAction mergeActions(ArrayDeque actionQueue) {
+ DownloadAction removeAction = null;
+ DownloadAction downloadAction = null;
+ HashSet keys = new HashSet<>();
+ boolean downloadAllTracks = false;
+ DownloadAction firstAction = Assertions.checkNotNull(actionQueue.peek());
+
+ while (!actionQueue.isEmpty()) {
+ DownloadAction action = actionQueue.remove();
+ Assertions.checkState(action.type.equals(firstAction.type));
+ Assertions.checkState(action.isSameMedia(firstAction));
+ if (action.isRemoveAction) {
+ removeAction = action;
+ downloadAction = null;
+ keys.clear();
+ downloadAllTracks = false;
+ } else {
+ if (!downloadAllTracks) {
+ if (action.keys.isEmpty()) {
+ downloadAllTracks = true;
+ keys.clear();
+ } else {
+ keys.addAll(action.keys);
+ }
+ }
+ downloadAction = action;
+ }
+ }
+
+ if (removeAction != null) {
+ actionQueue.add(removeAction);
+ }
+ if (downloadAction != null) {
+ actionQueue.add(
+ DownloadAction.createDownloadAction(
+ downloadAction.type,
+ downloadAction.uri,
+ new ArrayList<>(keys),
+ downloadAction.customCacheKey,
+ downloadAction.data));
+ }
+ return Assertions.checkNotNull(actionQueue.peek());
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java
index 044bd8cc8a..e799aff4b2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java
@@ -19,18 +19,66 @@ import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.Nullable;
+import android.util.SparseIntArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.BaseTrackSelection;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import com.google.android.exoplayer2.upstream.BandwidthMeter;
+import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* A helper for initializing and removing downloads.
*
+ * The helper extracts track information from the media, selects tracks for downloading, and
+ * creates {@link DownloadAction download actions} based on the selected tracks.
+ *
+ *
A typical usage of DownloadHelper follows these steps:
+ *
+ *
+ * - Construct the download helper with information about the {@link RenderersFactory renderers}
+ * and {@link DefaultTrackSelector.Parameters parameters} for track selection.
+ *
- Prepare the helper using {@link #prepare(Callback)} and wait for the callback.
+ *
- Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link
+ * #getTrackSelections(int, int)}, and make adjustments using {@link
+ * #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link
+ * #addTrackSelection(int, Parameters)}.
+ *
- Create download actions for the selected track using {@link #getDownloadAction(byte[])}.
+ *
+ *
* @param The manifest type.
*/
public abstract class DownloadHelper {
+ /**
+ * The default parameters used for track selection for downloading. This default selects the
+ * highest bitrate audio and video tracks which are supported by the renderers.
+ */
+ public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS =
+ new DefaultTrackSelector.ParametersBuilder().setForceHighestSupportedBitrate(true).build();
+
/** A callback to be notified when the {@link DownloadHelper} is prepared. */
public interface Callback {
@@ -39,7 +87,7 @@ public abstract class DownloadHelper {
*
* @param helper The reporting {@link DownloadHelper}.
*/
- void onPrepared(DownloadHelper helper);
+ void onPrepared(DownloadHelper> helper);
/**
* Called when preparation fails.
@@ -47,27 +95,51 @@ public abstract class DownloadHelper {
* @param helper The reporting {@link DownloadHelper}.
* @param e The error.
*/
- void onPrepareError(DownloadHelper helper, IOException e);
+ void onPrepareError(DownloadHelper> helper, IOException e);
}
private final String downloadType;
private final Uri uri;
@Nullable private final String cacheKey;
+ private final DefaultTrackSelector trackSelector;
+ private final RendererCapabilities[] rendererCapabilities;
+ private final SparseIntArray scratchSet;
+ private int currentTrackSelectionPeriodIndex;
@Nullable private T manifest;
- @Nullable private TrackGroupArray[] trackGroupArrays;
+ private TrackGroupArray @MonotonicNonNull [] trackGroupArrays;
+ private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos;
+ private List @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer;
+ private List @MonotonicNonNull [][] immutableTrackSelectionsByPeriodAndRenderer;
/**
- * Create download helper.
+ * Creates download helper.
*
* @param downloadType A download type. This value will be used as {@link DownloadAction#type}.
* @param uri A {@link Uri}.
* @param cacheKey An optional cache key.
+ * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
+ * downloading.
+ * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
+ * are selected.
+ * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
+ * {@code renderersFactory}.
*/
- public DownloadHelper(String downloadType, Uri uri, @Nullable String cacheKey) {
+ public DownloadHelper(
+ String downloadType,
+ Uri uri,
+ @Nullable String cacheKey,
+ DefaultTrackSelector.Parameters trackSelectorParameters,
+ RenderersFactory renderersFactory,
+ @Nullable DrmSessionManager drmSessionManager) {
this.downloadType = downloadType;
this.uri = uri;
this.cacheKey = cacheKey;
+ this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory());
+ this.rendererCapabilities = Util.getRendererCapabilities(renderersFactory, drmSessionManager);
+ this.scratchSet = new SparseIntArray();
+ trackSelector.setParameters(trackSelectorParameters);
+ trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter());
}
/**
@@ -77,21 +149,28 @@ public abstract class DownloadHelper {
* will be invoked on the calling thread unless that thread does not have an associated {@link
* Looper}, in which case it will be called on the application's main thread.
*/
- public final void prepare(final Callback callback) {
- final Handler handler =
+ public final void prepare(Callback callback) {
+ Handler handler =
new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper());
- new Thread() {
- @Override
- public void run() {
- try {
- manifest = loadManifest(uri);
- trackGroupArrays = getTrackGroupArrays(manifest);
- handler.post(() -> callback.onPrepared(DownloadHelper.this));
- } catch (final IOException e) {
- handler.post(() -> callback.onPrepareError(DownloadHelper.this, e));
- }
- }
- }.start();
+ new Thread(
+ () -> {
+ try {
+ manifest = loadManifest(uri);
+ trackGroupArrays = getTrackGroupArrays(manifest);
+ initializeTrackSelectionLists(trackGroupArrays.length, rendererCapabilities.length);
+ mappedTrackInfos = new MappedTrackInfo[trackGroupArrays.length];
+ for (int i = 0; i < trackGroupArrays.length; i++) {
+ TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i);
+ trackSelector.onSelectionActivated(trackSelectorResult.info);
+ mappedTrackInfos[i] =
+ Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
+ }
+ handler.post(() -> callback.onPrepared(DownloadHelper.this));
+ } catch (final IOException e) {
+ handler.post(() -> callback.onPrepareError(DownloadHelper.this, e));
+ }
+ })
+ .start();
}
/** Returns the manifest. Must not be called until after preparation completes. */
@@ -113,6 +192,8 @@ public abstract class DownloadHelper {
* Returns the track groups for the given period. Must not be called until after preparation
* completes.
*
+ * Use {@link #getMappedTrackInfo(int)} to get the track groups mapped to renderers.
+ *
* @param periodIndex The period index.
* @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream
* content.
@@ -123,16 +204,103 @@ public abstract class DownloadHelper {
}
/**
- * Builds a {@link DownloadAction} for downloading the specified tracks. Must not be called until
+ * Returns the mapped track info for the given period. Must not be called until after preparation
+ * completes.
+ *
+ * @param periodIndex The period index.
+ * @return The {@link MappedTrackInfo} for the period.
+ */
+ public final MappedTrackInfo getMappedTrackInfo(int periodIndex) {
+ Assertions.checkNotNull(mappedTrackInfos);
+ return mappedTrackInfos[periodIndex];
+ }
+
+ /**
+ * Returns all {@link TrackSelection track selections} for a period and renderer. Must not be
+ * called until after preparation completes.
+ *
+ * @param periodIndex The period index.
+ * @param rendererIndex The renderer index.
+ * @return A list of selected {@link TrackSelection track selections}.
+ */
+ public final List getTrackSelections(int periodIndex, int rendererIndex) {
+ Assertions.checkNotNull(immutableTrackSelectionsByPeriodAndRenderer);
+ return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
+ }
+
+ /**
+ * Clears the selection of tracks for a period. Must not be called until after preparation
+ * completes.
+ *
+ * @param periodIndex The period index for which track selections are cleared.
+ */
+ public final void clearTrackSelections(int periodIndex) {
+ Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
+ for (int i = 0; i < rendererCapabilities.length; i++) {
+ trackSelectionsByPeriodAndRenderer[periodIndex][i].clear();
+ }
+ }
+
+ /**
+ * Replaces a selection of tracks to be downloaded. Must not be called until after preparation
+ * completes.
+ *
+ * @param periodIndex The period index for which the track selection is replaced.
+ * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
+ * selection of tracks.
+ */
+ public final void replaceTrackSelections(
+ int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
+ clearTrackSelections(periodIndex);
+ addTrackSelection(periodIndex, trackSelectorParameters);
+ }
+
+ /**
+ * Adds a selection of tracks to be downloaded. Must not be called until after preparation
+ * completes.
+ *
+ * @param periodIndex The period index this track selection is added for.
+ * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new
+ * selection of tracks.
+ */
+ public final void addTrackSelection(
+ int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) {
+ Assertions.checkNotNull(trackGroupArrays);
+ Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
+ trackSelector.setParameters(trackSelectorParameters);
+ runTrackSelection(periodIndex);
+ }
+
+ /**
+ * Builds a {@link DownloadAction} for downloading the selected tracks. Must not be called until
* after preparation completes.
*
* @param data Application provided data to store in {@link DownloadAction#data}.
- * @param trackKeys The selected tracks. If empty, all streams will be downloaded.
* @return The built {@link DownloadAction}.
*/
- public final DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) {
- return DownloadAction.createDownloadAction(
- downloadType, uri, toStreamKeys(trackKeys), cacheKey, data);
+ public final DownloadAction getDownloadAction(@Nullable byte[] data) {
+ Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer);
+ Assertions.checkNotNull(trackGroupArrays);
+ List streamKeys = new ArrayList<>();
+ int periodCount = trackSelectionsByPeriodAndRenderer.length;
+ for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
+ int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length;
+ for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
+ List trackSelectionList =
+ trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
+ for (int selectionIndex = 0; selectionIndex < trackSelectionList.size(); selectionIndex++) {
+ TrackSelection trackSelection = trackSelectionList.get(selectionIndex);
+ int trackGroupIndex =
+ trackGroupArrays[periodIndex].indexOf(trackSelection.getTrackGroup());
+ int trackCount = trackSelection.length();
+ for (int trackListIndex = 0; trackListIndex < trackCount; trackListIndex++) {
+ int trackIndex = trackSelection.getIndexInTrackGroup(trackListIndex);
+ streamKeys.add(toStreamKey(periodIndex, trackGroupIndex, trackIndex));
+ }
+ }
+ }
+ }
+ return DownloadAction.createDownloadAction(downloadType, uri, streamKeys, cacheKey, data);
}
/**
@@ -161,10 +329,151 @@ public abstract class DownloadHelper {
protected abstract TrackGroupArray[] getTrackGroupArrays(T manifest);
/**
- * Converts a list of {@link TrackKey track keys} to {@link StreamKey stream keys}.
+ * Converts a track of a track group of a period to the corresponding {@link StreamKey}.
*
- * @param trackKeys A list of track keys.
- * @return A corresponding list of stream keys.
+ * @param periodIndex The index of the containing period.
+ * @param trackGroupIndex The index of the containing track group within the period.
+ * @param trackIndexInTrackGroup The index of the track within the track group.
+ * @return The corresponding {@link StreamKey}.
*/
- protected abstract List toStreamKeys(List trackKeys);
+ protected abstract StreamKey toStreamKey(
+ int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup);
+
+ @SuppressWarnings("unchecked")
+ @EnsuresNonNull("trackSelectionsByPeriodAndRenderer")
+ private void initializeTrackSelectionLists(int periodCount, int rendererCount) {
+ trackSelectionsByPeriodAndRenderer =
+ (List[][]) new List>[periodCount][rendererCount];
+ immutableTrackSelectionsByPeriodAndRenderer =
+ (List[][]) new List>[periodCount][rendererCount];
+ for (int i = 0; i < periodCount; i++) {
+ for (int j = 0; j < rendererCount; j++) {
+ trackSelectionsByPeriodAndRenderer[i][j] = new ArrayList<>();
+ immutableTrackSelectionsByPeriodAndRenderer[i][j] =
+ Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]);
+ }
+ }
+ }
+
+ /**
+ * Runs the track selection for a given period index with the current parameters. The selected
+ * tracks will be added to {@link #trackSelectionsByPeriodAndRenderer}.
+ */
+ // Intentional reference comparison of track group instances.
+ @SuppressWarnings("ReferenceEquality")
+ @RequiresNonNull({"trackGroupArrays", "trackSelectionsByPeriodAndRenderer"})
+ private TrackSelectorResult runTrackSelection(int periodIndex) {
+ // TODO: Use actual timeline and media period id.
+ MediaPeriodId dummyMediaPeriodId = new MediaPeriodId(new Object());
+ Timeline dummyTimeline = Timeline.EMPTY;
+ currentTrackSelectionPeriodIndex = periodIndex;
+ try {
+ TrackSelectorResult trackSelectorResult =
+ trackSelector.selectTracks(
+ rendererCapabilities,
+ trackGroupArrays[periodIndex],
+ dummyMediaPeriodId,
+ dummyTimeline);
+ for (int i = 0; i < trackSelectorResult.length; i++) {
+ TrackSelection newSelection = trackSelectorResult.selections.get(i);
+ if (newSelection == null) {
+ continue;
+ }
+ List existingSelectionList =
+ trackSelectionsByPeriodAndRenderer[currentTrackSelectionPeriodIndex][i];
+ boolean mergedWithExistingSelection = false;
+ for (int j = 0; j < existingSelectionList.size(); j++) {
+ TrackSelection existingSelection = existingSelectionList.get(j);
+ if (existingSelection.getTrackGroup() == newSelection.getTrackGroup()) {
+ // Merge with existing selection.
+ scratchSet.clear();
+ for (int k = 0; k < existingSelection.length(); k++) {
+ scratchSet.put(existingSelection.getIndexInTrackGroup(k), 0);
+ }
+ for (int k = 0; k < newSelection.length(); k++) {
+ scratchSet.put(newSelection.getIndexInTrackGroup(k), 0);
+ }
+ int[] mergedTracks = new int[scratchSet.size()];
+ for (int k = 0; k < scratchSet.size(); k++) {
+ mergedTracks[k] = scratchSet.keyAt(k);
+ }
+ existingSelectionList.set(
+ j, new DownloadTrackSelection(existingSelection.getTrackGroup(), mergedTracks));
+ mergedWithExistingSelection = true;
+ break;
+ }
+ }
+ if (!mergedWithExistingSelection) {
+ existingSelectionList.add(newSelection);
+ }
+ }
+ return trackSelectorResult;
+ } catch (ExoPlaybackException e) {
+ // DefaultTrackSelector does not throw exceptions during track selection.
+ throw new UnsupportedOperationException(e);
+ }
+ }
+
+ private static final class DownloadTrackSelection extends BaseTrackSelection {
+
+ private static final class Factory implements TrackSelection.Factory {
+
+ @Override
+ public @NullableType TrackSelection[] createTrackSelections(
+ @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
+ @NullableType TrackSelection[] selections = new TrackSelection[definitions.length];
+ for (int i = 0; i < definitions.length; i++) {
+ selections[i] =
+ definitions[i] == null
+ ? null
+ : new DownloadTrackSelection(definitions[i].group, definitions[i].tracks);
+ }
+ return selections;
+ }
+ }
+
+ public DownloadTrackSelection(TrackGroup trackGroup, int[] tracks) {
+ super(trackGroup, tracks);
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return 0;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return C.SELECTION_REASON_UNKNOWN;
+ }
+
+ @Nullable
+ @Override
+ public Object getSelectionData() {
+ return null;
+ }
+ }
+
+ private static final class DummyBandwidthMeter implements BandwidthMeter {
+
+ @Override
+ public long getBitrateEstimate() {
+ return 0;
+ }
+
+ @Nullable
+ @Override
+ public TransferListener getTransferListener() {
+ return null;
+ }
+
+ @Override
+ public void addEventListener(Handler eventHandler, EventListener eventListener) {
+ // Do nothing.
+ }
+
+ @Override
+ public void removeEventListener(EventListener eventListener) {
+ // Do nothing.
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java
new file mode 100644
index 0000000000..7b903d3321
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+import android.support.annotation.Nullable;
+
+/** Persists {@link DownloadState}s. */
+interface DownloadIndex {
+
+ /**
+ * Returns the {@link DownloadState} with the given {@code id}, or null.
+ *
+ * @param id ID of a {@link DownloadState}.
+ * @return The {@link DownloadState} with the given {@code id}, or null if a download state with
+ * this id doesn't exist.
+ */
+ @Nullable
+ DownloadState getDownloadState(String id);
+
+ /**
+ * Returns a {@link DownloadStateCursor} to {@link DownloadState}s with the given {@code states}.
+ *
+ * @param states Returns only the {@link DownloadState}s with this states. If empty, returns all.
+ * @return A cursor to {@link DownloadState}s with the given {@code states}.
+ */
+ DownloadStateCursor getDownloadStates(@DownloadState.State int... states);
+
+ /**
+ * Adds or replaces a {@link DownloadState}.
+ *
+ * @param downloadState The {@link DownloadState} to be added.
+ */
+ void putDownloadState(DownloadState downloadState);
+
+ /** Removes the {@link DownloadState} with the given {@code id}. */
+ void removeDownloadState(String id);
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java
new file mode 100644
index 0000000000..63602c7641
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.offline.DownloadState.State;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+
+/** {@link DownloadIndex} related utility methods. */
+public final class DownloadIndexUtil {
+
+ /** An interface to provide custom download ids during ActionFile upgrade. */
+ public interface DownloadIdProvider {
+
+ /**
+ * Returns a custom download id for given action.
+ *
+ * @param downloadAction The action which is an id requested for.
+ * @return A custom download id for given action.
+ */
+ String getId(DownloadAction downloadAction);
+ }
+
+ private DownloadIndexUtil() {}
+
+ /**
+ * Upgrades an {@link ActionFile} to {@link DownloadIndex}.
+ *
+ * This method shouldn't be called while {@link DownloadIndex} is used by {@link
+ * DownloadManager}.
+ *
+ * @param actionFile The action file to upgrade.
+ * @param downloadIndex Actions are converted to {@link DownloadState}s and stored in this index.
+ * @param downloadIdProvider A nullable custom download id provider.
+ * @throws IOException If there is an error during loading actions.
+ */
+ public static void upgradeActionFile(
+ ActionFile actionFile,
+ DownloadIndex downloadIndex,
+ @Nullable DownloadIdProvider downloadIdProvider)
+ throws IOException {
+ if (downloadIdProvider == null) {
+ downloadIdProvider = downloadAction -> downloadAction.id;
+ }
+ for (DownloadAction action : actionFile.load()) {
+ addAction(downloadIndex, downloadIdProvider.getId(action), action);
+ }
+ }
+
+ /**
+ * Converts a {@link DownloadAction} to {@link DownloadState} and stored in the given {@link
+ * DownloadIndex}.
+ *
+ *
This method shouldn't be called while {@link DownloadIndex} is used by {@link
+ * DownloadManager}.
+ *
+ * @param downloadIndex The action is converted to {@link DownloadState} and stored in this index.
+ * @param id A nullable custom download id which overwrites {@link DownloadAction#id}.
+ * @param action The action to be stored in {@link DownloadIndex}.
+ */
+ public static void addAction(
+ DownloadIndex downloadIndex, @Nullable String id, DownloadAction action) {
+ DownloadState downloadState = downloadIndex.getDownloadState(id != null ? id : action.id);
+ if (downloadState != null) {
+ downloadState = merge(downloadState, action);
+ } else {
+ downloadState = convert(action);
+ }
+ downloadIndex.putDownloadState(downloadState);
+ }
+
+ private static DownloadState merge(DownloadState downloadState, DownloadAction action) {
+ Assertions.checkArgument(action.type.equals(downloadState.type));
+ @State int newState;
+ if (action.isRemoveAction) {
+ newState = DownloadState.STATE_REMOVING;
+ } else {
+ if (downloadState.state == DownloadState.STATE_REMOVING
+ || downloadState.state == DownloadState.STATE_RESTARTING) {
+ newState = DownloadState.STATE_RESTARTING;
+ } else if (downloadState.state == DownloadState.STATE_STOPPED) {
+ newState = DownloadState.STATE_STOPPED;
+ } else {
+ newState = DownloadState.STATE_QUEUED;
+ }
+ }
+ HashSet keys = new HashSet<>(action.keys);
+ Collections.addAll(keys, downloadState.streamKeys);
+ StreamKey[] newKeys = keys.toArray(new StreamKey[0]);
+ return new DownloadState(
+ downloadState.id,
+ downloadState.type,
+ action.uri,
+ action.customCacheKey,
+ newState,
+ /* downloadPercentage= */ C.PERCENTAGE_UNSET,
+ downloadState.downloadedBytes,
+ /* totalBytes= */ C.LENGTH_UNSET,
+ downloadState.failureReason,
+ downloadState.stopFlags,
+ downloadState.startTimeMs,
+ downloadState.updateTimeMs,
+ newKeys,
+ action.data);
+ }
+
+ private static DownloadState convert(DownloadAction action) {
+ long currentTimeMs = System.currentTimeMillis();
+ return new DownloadState(
+ action.id,
+ action.type,
+ action.uri,
+ action.customCacheKey,
+ /* state= */ action.isRemoveAction
+ ? DownloadState.STATE_REMOVING
+ : DownloadState.STATE_QUEUED,
+ /* downloadPercentage= */ C.PERCENTAGE_UNSET,
+ /* downloadedBytes= */ 0,
+ /* totalBytes= */ C.LENGTH_UNSET,
+ DownloadState.FAILURE_REASON_NONE,
+ /* stopFlags= */ 0,
+ /* startTimeMs= */ currentTimeMs,
+ /* updateTimeMs= */ currentTimeMs,
+ action.keys.toArray(new StreamKey[0]),
+ action.data);
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
index 4a76c80d64..8932140a34 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
@@ -15,28 +15,34 @@
*/
package com.google.android.exoplayer2.offline;
-import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_CANCELED;
-import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_COMPLETED;
-import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_FAILED;
-import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_QUEUED;
-import static com.google.android.exoplayer2.offline.DownloadManager.TaskState.STATE_STARTED;
+import static com.google.android.exoplayer2.offline.DownloadState.FAILURE_REASON_NONE;
+import static com.google.android.exoplayer2.offline.DownloadState.FAILURE_REASON_UNKNOWN;
+import static com.google.android.exoplayer2.offline.DownloadState.STATE_COMPLETED;
+import static com.google.android.exoplayer2.offline.DownloadState.STATE_DOWNLOADING;
+import static com.google.android.exoplayer2.offline.DownloadState.STATE_FAILED;
+import static com.google.android.exoplayer2.offline.DownloadState.STATE_QUEUED;
+import static com.google.android.exoplayer2.offline.DownloadState.STATE_REMOVED;
+import static com.google.android.exoplayer2.offline.DownloadState.STATE_REMOVING;
+import static com.google.android.exoplayer2.offline.DownloadState.STATE_RESTARTING;
+import static com.google.android.exoplayer2.offline.DownloadState.STATE_STOPPED;
+import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY;
+import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_STOPPED;
+import android.content.Context;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
-import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.scheduler.Requirements;
+import com.google.android.exoplayer2.scheduler.RequirementsWatcher;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import java.io.File;
import java.io.IOException;
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayDeque;
import java.util.ArrayList;
-import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@@ -58,77 +64,106 @@ public final class DownloadManager {
*/
void onInitialized(DownloadManager downloadManager);
/**
- * Called when the state of a task changes.
+ * Called when the state of a download changes.
*
* @param downloadManager The reporting instance.
- * @param taskState The state of the task.
+ * @param downloadState The state of the download.
*/
- void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState);
+ void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState);
/**
- * Called when there is no active task left.
+ * Called when there is no active download left.
*
* @param downloadManager The reporting instance.
*/
void onIdle(DownloadManager downloadManager);
+
+ /**
+ * Called when the download requirements state changed.
+ *
+ * @param downloadManager The reporting instance.
+ * @param requirements Requirements needed to be met to start downloads.
+ * @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not
+ * met, or 0.
+ */
+ void onRequirementsStateChanged(
+ DownloadManager downloadManager,
+ Requirements requirements,
+ @Requirements.RequirementFlags int notMetRequirements);
}
- /** The default maximum number of simultaneous download tasks. */
+ /** The default maximum number of simultaneous downloads. */
public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1;
- /** The default minimum number of times a task must be retried before failing. */
+ /** The default minimum number of times a download must be retried before failing. */
public static final int DEFAULT_MIN_RETRY_COUNT = 5;
+ /** The default requirement is that the device has network connectivity. */
+ public static final Requirements DEFAULT_REQUIREMENTS =
+ new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
private static final String TAG = "DownloadManager";
private static final boolean DEBUG = false;
- private final int maxActiveDownloadTasks;
+ private final int maxActiveDownloads;
private final int minRetryCount;
+ private final Context context;
private final ActionFile actionFile;
private final DownloaderFactory downloaderFactory;
- private final ArrayList tasks;
- private final ArrayList activeDownloadTasks;
+ private final ArrayList downloads;
+ private final ArrayList activeDownloads;
private final Handler handler;
private final HandlerThread fileIOThread;
private final Handler fileIOHandler;
private final CopyOnWriteArraySet listeners;
+ private final ArrayDeque actionQueue;
- private int nextTaskId;
private boolean initialized;
private boolean released;
- private boolean downloadsStopped;
+ @DownloadState.StopFlags private int stickyStopFlags;
+ private RequirementsWatcher requirementsWatcher;
/**
* Constructs a {@link DownloadManager}.
*
+ * @param context Any context.
* @param actionFile The file in which active actions are saved.
* @param downloaderFactory A factory for creating {@link Downloader}s.
*/
- public DownloadManager(File actionFile, DownloaderFactory downloaderFactory) {
+ public DownloadManager(Context context, File actionFile, DownloaderFactory downloaderFactory) {
this(
- actionFile, downloaderFactory, DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, DEFAULT_MIN_RETRY_COUNT);
+ context,
+ actionFile,
+ downloaderFactory,
+ DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS,
+ DEFAULT_MIN_RETRY_COUNT,
+ DEFAULT_REQUIREMENTS);
}
/**
* Constructs a {@link DownloadManager}.
*
+ * @param context Any context.
* @param actionFile The file in which active actions are saved.
* @param downloaderFactory A factory for creating {@link Downloader}s.
- * @param maxSimultaneousDownloads The maximum number of simultaneous download tasks.
- * @param minRetryCount The minimum number of times a task must be retried before failing.
+ * @param maxSimultaneousDownloads The maximum number of simultaneous downloads.
+ * @param minRetryCount The minimum number of times a download must be retried before failing.
+ * @param requirements The requirements needed to be met to start downloads.
*/
public DownloadManager(
+ Context context,
File actionFile,
DownloaderFactory downloaderFactory,
int maxSimultaneousDownloads,
- int minRetryCount) {
+ int minRetryCount,
+ Requirements requirements) {
+ this.context = context.getApplicationContext();
this.actionFile = new ActionFile(actionFile);
this.downloaderFactory = downloaderFactory;
- this.maxActiveDownloadTasks = maxSimultaneousDownloads;
+ this.maxActiveDownloads = maxSimultaneousDownloads;
this.minRetryCount = minRetryCount;
- this.downloadsStopped = true;
+ this.stickyStopFlags = STOP_FLAG_STOPPED | STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY;
- tasks = new ArrayList<>();
- activeDownloadTasks = new ArrayList<>();
+ downloads = new ArrayList<>();
+ activeDownloads = new ArrayList<>();
Looper looper = Looper.myLooper();
if (looper == null) {
@@ -141,11 +176,32 @@ public final class DownloadManager {
fileIOHandler = new Handler(fileIOThread.getLooper());
listeners = new CopyOnWriteArraySet<>();
+ actionQueue = new ArrayDeque<>();
+ watchRequirements(requirements);
loadActions();
logd("Created");
}
+ /**
+ * Sets the requirements needed to be met to start downloads.
+ *
+ * @param requirements Need to be met to start downloads.
+ */
+ public void setRequirements(Requirements requirements) {
+ Assertions.checkState(!released);
+ if (requirements.equals(requirementsWatcher.getRequirements())) {
+ return;
+ }
+ requirementsWatcher.stop();
+ notifyListenersRequirementsStateChange(watchRequirements(requirements));
+ }
+
+ /** Returns the requirements needed to be met to start downloads. */
+ public Requirements getRequirements() {
+ return requirementsWatcher.getRequirements();
+ }
+
/**
* Adds a {@link Listener}.
*
@@ -164,85 +220,81 @@ public final class DownloadManager {
listeners.remove(listener);
}
- /** Starts the download tasks. */
+ /** Starts the downloads. */
public void startDownloads() {
- Assertions.checkState(!released);
- if (downloadsStopped) {
- downloadsStopped = false;
- maybeStartTasks();
- logd("Downloads are started");
- }
+ clearStopFlags(STOP_FLAG_STOPPED);
}
- /** Stops all of the download tasks. Call {@link #startDownloads()} to restart tasks. */
+ /** Stops all of the downloads. Call {@link #startDownloads()} to restart downloads. */
public void stopDownloads() {
+ setStopFlags(STOP_FLAG_STOPPED);
+ }
+
+ private void setStopFlags(int flags) {
+ updateStopFlags(flags, flags);
+ }
+
+ private void clearStopFlags(int flags) {
+ updateStopFlags(flags, 0);
+ }
+
+ private void updateStopFlags(int flags, int values) {
Assertions.checkState(!released);
- if (!downloadsStopped) {
- downloadsStopped = true;
- for (int i = 0; i < activeDownloadTasks.size(); i++) {
- activeDownloadTasks.get(i).stop();
+ int updatedStickyStopFlags = (values & flags) | (stickyStopFlags & ~flags);
+ if (stickyStopFlags != updatedStickyStopFlags) {
+ stickyStopFlags = updatedStickyStopFlags;
+ for (int i = 0; i < downloads.size(); i++) {
+ downloads.get(i).updateStopFlags(flags, values);
}
- logd("Downloads are stopping");
+ logdFlags("Sticky stop flags are updated", updatedStickyStopFlags);
}
}
/**
- * Handles the given action. A task is created and added to the task queue. If it's a remove
- * action then any download tasks for the same media are immediately canceled.
+ * Handles the given action.
*
* @param action The action to be executed.
- * @return The id of the newly created task.
*/
- public int handleAction(DownloadAction action) {
+ public void handleAction(DownloadAction action) {
Assertions.checkState(!released);
- Task task = addTaskForAction(action);
if (initialized) {
+ addDownloadForAction(action);
saveActions();
- maybeStartTasks();
- if (task.state == STATE_QUEUED) {
- // Task did not change out of its initial state, and so its initial state won't have been
- // reported to listeners. Do so now.
- notifyListenersTaskStateChange(task);
- }
+ } else {
+ actionQueue.add(action);
}
- return task.id;
}
- /** Returns the number of tasks. */
- public int getTaskCount() {
- Assertions.checkState(!released);
- return tasks.size();
- }
-
- /** Returns the number of download tasks. */
+ /** Returns the number of downloads. */
public int getDownloadCount() {
- int count = 0;
- for (int i = 0; i < tasks.size(); i++) {
- if (!tasks.get(i).action.isRemoveAction) {
- count++;
- }
- }
- return count;
+ Assertions.checkState(!released);
+ return downloads.size();
}
- /** Returns the state of a task, or null if no such task exists */
- public @Nullable TaskState getTaskState(int taskId) {
+ /**
+ * Returns {@link DownloadState} for the given content id, or null if no such download exists.
+ *
+ * @param id The unique content id.
+ * @return DownloadState for the given content id, or null if no such download exists.
+ */
+ @Nullable
+ public DownloadState getDownloadState(String id) {
Assertions.checkState(!released);
- for (int i = 0; i < tasks.size(); i++) {
- Task task = tasks.get(i);
- if (task.id == taskId) {
- return task.getTaskState();
+ for (int i = 0; i < downloads.size(); i++) {
+ Download download = downloads.get(i);
+ if (download.id.equals(id)) {
+ return download.getDownloadState();
}
}
return null;
}
- /** Returns the states of all current tasks. */
- public TaskState[] getAllTaskStates() {
+ /** Returns the states of all current downloads. */
+ public DownloadState[] getAllDownloadStates() {
Assertions.checkState(!released);
- TaskState[] states = new TaskState[tasks.size()];
+ DownloadState[] states = new DownloadState[downloads.size()];
for (int i = 0; i < states.length; i++) {
- states[i] = tasks.get(i).getTaskState();
+ states[i] = downloads.get(i).getDownloadState();
}
return states;
}
@@ -253,14 +305,14 @@ public final class DownloadManager {
return initialized;
}
- /** Returns whether there are no active tasks. */
+ /** Returns whether there are no active downloads. */
public boolean isIdle() {
Assertions.checkState(!released);
if (!initialized) {
return false;
}
- for (int i = 0; i < tasks.size(); i++) {
- if (tasks.get(i).isStarted()) {
+ for (int i = 0; i < downloads.size(); i++) {
+ if (!downloads.get(i).isIdle()) {
return false;
}
}
@@ -268,16 +320,18 @@ public final class DownloadManager {
}
/**
- * Stops all of the tasks and releases resources. If the action file isn't up to date, waits for
- * the changes to be written. The manager must not be accessed after this method has been called.
+ * Stops all of the downloads and releases resources. If the action file isn't up to date, waits
+ * for the changes to be written. The manager must not be accessed after this method has been
+ * called.
*/
public void release() {
if (released) {
return;
}
+ setStopFlags(STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY);
released = true;
- for (int i = 0; i < tasks.size(); i++) {
- tasks.get(i).stop();
+ if (requirementsWatcher != null) {
+ requirementsWatcher.stop();
}
final ConditionVariable fileIOFinishedCondition = new ConditionVariable();
fileIOHandler.post(fileIOFinishedCondition::open);
@@ -286,67 +340,24 @@ public final class DownloadManager {
logd("Released");
}
- private Task addTaskForAction(DownloadAction action) {
- Task task = new Task(nextTaskId++, this, downloaderFactory, action, minRetryCount);
- tasks.add(task);
- logd("Task is added", task);
- return task;
+ private void addDownloadForAction(DownloadAction action) {
+ for (int i = 0; i < downloads.size(); i++) {
+ Download download = downloads.get(i);
+ if (download.addAction(action)) {
+ logd("Action is added to existing download", download);
+ return;
+ }
+ }
+ Download download =
+ new Download(this, downloaderFactory, action, minRetryCount, stickyStopFlags);
+ downloads.add(download);
+ logd("Download is added", download);
}
- /**
- * Iterates through the task queue and starts any task if all of the following are true:
- *
- *
- * - It hasn't started yet.
- *
- There are no preceding conflicting tasks.
- *
- If it's a download task then there are no preceding download tasks on hold and the
- * maximum number of active downloads hasn't been reached.
- *
- *
- * If the task is a remove action then preceding conflicting tasks are canceled.
- */
- private void maybeStartTasks() {
- if (!initialized || released) {
- return;
- }
-
- boolean skipDownloadActions = downloadsStopped
- || activeDownloadTasks.size() == maxActiveDownloadTasks;
- for (int i = 0; i < tasks.size(); i++) {
- Task task = tasks.get(i);
- if (!task.canStart()) {
- continue;
- }
-
- DownloadAction action = task.action;
- boolean isRemoveAction = action.isRemoveAction;
- if (!isRemoveAction && skipDownloadActions) {
- continue;
- }
-
- boolean canStartTask = true;
- for (int j = 0; j < i; j++) {
- Task otherTask = tasks.get(j);
- if (otherTask.action.isSameMedia(action)) {
- if (isRemoveAction) {
- canStartTask = false;
- logd(task + " clashes with " + otherTask);
- otherTask.cancel();
- // Continue loop to cancel any other preceding clashing tasks.
- } else if (otherTask.action.isRemoveAction) {
- canStartTask = false;
- skipDownloadActions = true;
- break;
- }
- }
- }
-
- if (canStartTask) {
- task.start();
- if (!isRemoveAction) {
- activeDownloadTasks.add(task);
- skipDownloadActions = activeDownloadTasks.size() == maxActiveDownloadTasks;
- }
+ private void maybeStartDownload(Download download) {
+ if (activeDownloads.size() < maxActiveDownloads) {
+ if (download.start()) {
+ activeDownloads.add(download);
}
}
}
@@ -361,30 +372,41 @@ public final class DownloadManager {
}
}
- private void onTaskStateChange(Task task) {
+ private void onDownloadStateChange(Download download) {
if (released) {
return;
}
- boolean stopped = !task.isStarted();
- if (stopped) {
- activeDownloadTasks.remove(task);
+ boolean idle = download.isIdle();
+ if (idle) {
+ activeDownloads.remove(download);
}
- notifyListenersTaskStateChange(task);
- if (task.isFinished()) {
- tasks.remove(task);
+ notifyListenersDownloadStateChange(download);
+ if (download.isFinished()) {
+ downloads.remove(download);
saveActions();
}
- if (stopped) {
- maybeStartTasks();
+ if (idle) {
+ for (int i = 0; i < downloads.size(); i++) {
+ maybeStartDownload(downloads.get(i));
+ }
maybeNotifyListenersIdle();
}
}
- private void notifyListenersTaskStateChange(Task task) {
- logd("Task state is changed", task);
- TaskState taskState = task.getTaskState();
+ private void notifyListenersDownloadStateChange(Download download) {
+ logd("Download state is changed", download);
+ DownloadState downloadState = download.getDownloadState();
for (Listener listener : listeners) {
- listener.onTaskStateChanged(this, taskState);
+ listener.onDownloadStateChanged(this, downloadState);
+ }
+ }
+
+ private void notifyListenersRequirementsStateChange(
+ @Requirements.RequirementFlags int notMetRequirements) {
+ logdFlags("Not met requirements are changed", notMetRequirements);
+ for (Listener listener : listeners) {
+ listener.onRequirementsStateChanged(
+ DownloadManager.this, requirementsWatcher.getRequirements(), notMetRequirements);
}
}
@@ -405,29 +427,21 @@ public final class DownloadManager {
if (released) {
return;
}
- List pendingTasks = new ArrayList<>(tasks);
- tasks.clear();
for (DownloadAction action : actions) {
- addTaskForAction(action);
+ addDownloadForAction(action);
}
- logd("Tasks are created.");
+ if (!actionQueue.isEmpty()) {
+ while (!actionQueue.isEmpty()) {
+ addDownloadForAction(actionQueue.remove());
+ }
+ saveActions();
+ }
+ logd("Downloads are created.");
initialized = true;
for (Listener listener : listeners) {
listener.onInitialized(DownloadManager.this);
}
- if (!pendingTasks.isEmpty()) {
- tasks.addAll(pendingTasks);
- saveActions();
- }
- maybeStartTasks();
- for (int i = 0; i < tasks.size(); i++) {
- Task task = tasks.get(i);
- if (task.state == STATE_QUEUED) {
- // Task did not change out of its initial state, and so its initial state
- // won't have been reported to listeners. Do so now.
- notifyListenersTaskStateChange(task);
- }
- }
+ clearStopFlags(STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY);
});
});
}
@@ -436,14 +450,15 @@ public final class DownloadManager {
if (released) {
return;
}
- final DownloadAction[] actions = new DownloadAction[tasks.size()];
- for (int i = 0; i < tasks.size(); i++) {
- actions[i] = tasks.get(i).action;
+ ArrayList actions = new ArrayList<>(downloads.size());
+ for (int i = 0; i < downloads.size(); i++) {
+ actions.addAll(downloads.get(i).actionQueue);
}
+ final DownloadAction[] actionsArray = actions.toArray(new DownloadAction[0]);
fileIOHandler.post(
() -> {
try {
- actionFile.store(actions);
+ actionFile.store(actionsArray);
logd("Actions persisted.");
} catch (IOException e) {
Log.e(TAG, "Persisting actions failed.", e);
@@ -457,242 +472,287 @@ public final class DownloadManager {
}
}
- private static void logd(String message, Task task) {
- logd(message + ": " + task);
+ private static void logd(String message, Download download) {
+ if (DEBUG) {
+ logd(message + ": " + download);
+ }
}
- /** Represents state of a task. */
- public static final class TaskState {
-
- /**
- * Task states. One of {@link #STATE_QUEUED}, {@link #STATE_STARTED}, {@link #STATE_COMPLETED},
- * {@link #STATE_CANCELED} or {@link #STATE_FAILED}.
- *
- * Transition diagram:
- *
- *
- * ┌────────┬─────→ canceled
- * queued ↔ started ┬→ completed
- * └→ failed
- *
- */
- @Documented
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({STATE_QUEUED, STATE_STARTED, STATE_COMPLETED, STATE_CANCELED, STATE_FAILED})
- public @interface State {}
- /** The task is waiting to be started. */
- public static final int STATE_QUEUED = 0;
- /** The task is currently started. */
- public static final int STATE_STARTED = 1;
- /** The task completed. */
- public static final int STATE_COMPLETED = 2;
- /** The task was canceled. */
- public static final int STATE_CANCELED = 3;
- /** The task failed. */
- public static final int STATE_FAILED = 4;
-
- /** Returns the state string for the given state value. */
- public static String getStateString(@State int state) {
- switch (state) {
- case STATE_QUEUED:
- return "QUEUED";
- case STATE_STARTED:
- return "STARTED";
- case STATE_COMPLETED:
- return "COMPLETED";
- case STATE_CANCELED:
- return "CANCELED";
- case STATE_FAILED:
- return "FAILED";
- default:
- throw new IllegalStateException();
- }
+ private static void logdFlags(String message, int flags) {
+ if (DEBUG) {
+ logd(message + ": " + Integer.toBinaryString(flags));
}
-
- /** The unique task id. */
- public final int taskId;
- /** The action being executed. */
- public final DownloadAction action;
- /** The state of the task. */
- public final @State int state;
-
- /**
- * The estimated download percentage, or {@link C#PERCENTAGE_UNSET} if no estimate is available
- * or if this is a removal task.
- */
- public final float downloadPercentage;
- /** The total number of downloaded bytes. */
- public final long downloadedBytes;
-
- /** If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise null. */
- @Nullable public final Throwable error;
-
- private TaskState(
- int taskId,
- DownloadAction action,
- @State int state,
- float downloadPercentage,
- long downloadedBytes,
- @Nullable Throwable error) {
- this.taskId = taskId;
- this.action = action;
- this.state = state;
- this.downloadPercentage = downloadPercentage;
- this.downloadedBytes = downloadedBytes;
- this.error = error;
- }
-
}
- private static final class Task implements Runnable {
+ @Requirements.RequirementFlags
+ private int watchRequirements(Requirements requirements) {
+ requirementsWatcher = new RequirementsWatcher(context, new RequirementListener(), requirements);
+ @Requirements.RequirementFlags int notMetRequirements = requirementsWatcher.start();
+ if (notMetRequirements == 0) {
+ startDownloads();
+ } else {
+ stopDownloads();
+ }
+ return notMetRequirements;
+ }
- /** Target states for the download thread. */
- @Documented
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({STATE_COMPLETED, STATE_QUEUED, STATE_CANCELED})
- public @interface TargetState {}
+ private static final class Download {
- private final int id;
+ private final String id;
private final DownloadManager downloadManager;
private final DownloaderFactory downloaderFactory;
- private final DownloadAction action;
private final int minRetryCount;
- /** The current state of the task. */
- @TaskState.State private int state;
- /**
- * When started, this is the target state that the task will transition to when the download
- * thread stops.
- */
- @TargetState private volatile int targetState;
+ private final long startTimeMs;
+ private final ArrayDeque actionQueue;
+ /** The current state of the download. */
+ @DownloadState.State private int state;
@MonotonicNonNull private Downloader downloader;
- @MonotonicNonNull private Thread thread;
- @MonotonicNonNull private Throwable error;
+ @MonotonicNonNull private DownloadThread downloadThread;
+ @MonotonicNonNull @DownloadState.FailureReason private int failureReason;
+ @DownloadState.StopFlags private int stopFlags;
- private Task(
- int id,
+ private Download(
DownloadManager downloadManager,
DownloaderFactory downloaderFactory,
DownloadAction action,
- int minRetryCount) {
- this.id = id;
+ int minRetryCount,
+ int stopFlags) {
+ this.id = action.id;
this.downloadManager = downloadManager;
this.downloaderFactory = downloaderFactory;
- this.action = action;
this.minRetryCount = minRetryCount;
- state = STATE_QUEUED;
- targetState = STATE_COMPLETED;
+ this.stopFlags = stopFlags;
+ this.startTimeMs = System.currentTimeMillis();
+ actionQueue = new ArrayDeque<>();
+ actionQueue.add(action);
+ initialize(/* restart= */ false);
}
- public TaskState getTaskState() {
+ public boolean addAction(DownloadAction newAction) {
+ DownloadAction action = actionQueue.peek();
+ if (!action.isSameMedia(newAction)) {
+ return false;
+ }
+ Assertions.checkState(action.type.equals(newAction.type));
+ actionQueue.add(newAction);
+ DownloadAction updatedAction = DownloadActionUtil.mergeActions(actionQueue);
+ if (state == STATE_REMOVING) {
+ Assertions.checkState(updatedAction.isRemoveAction);
+ if (actionQueue.size() > 1) {
+ setState(STATE_RESTARTING);
+ }
+ } else if (state == STATE_RESTARTING) {
+ Assertions.checkState(updatedAction.isRemoveAction);
+ if (actionQueue.size() == 1) {
+ setState(STATE_REMOVING);
+ }
+ } else if (!action.equals(updatedAction)) {
+ if (state == STATE_DOWNLOADING) {
+ stopDownloadThread();
+ } else {
+ Assertions.checkState(state == STATE_QUEUED || state == STATE_STOPPED);
+ initialize(/* restart= */ false);
+ }
+ }
+ return true;
+ }
+
+ public DownloadState getDownloadState() {
float downloadPercentage = C.PERCENTAGE_UNSET;
long downloadedBytes = 0;
+ long totalBytes = C.LENGTH_UNSET;
if (downloader != null) {
downloadPercentage = downloader.getDownloadPercentage();
downloadedBytes = downloader.getDownloadedBytes();
+ totalBytes = downloader.getTotalBytes();
}
- return new TaskState(id, action, state, downloadPercentage, downloadedBytes, error);
+ DownloadAction action = actionQueue.peek();
+ return new DownloadState(
+ action.id,
+ action.type,
+ action.uri,
+ action.customCacheKey,
+ state,
+ downloadPercentage,
+ downloadedBytes,
+ totalBytes,
+ failureReason,
+ stopFlags,
+ startTimeMs,
+ /* updateTimeMs= */ System.currentTimeMillis(),
+ action.keys.toArray(new StreamKey[0]),
+ action.data);
}
- /** Returns whether the task is finished. */
public boolean isFinished() {
- return state == STATE_FAILED || state == STATE_COMPLETED || state == STATE_CANCELED;
+ return state == STATE_FAILED || state == STATE_COMPLETED || state == STATE_REMOVED;
}
- /** Returns whether the task is started. */
- public boolean isStarted() {
- return state == STATE_STARTED;
+ public boolean isIdle() {
+ return state != STATE_DOWNLOADING && state != STATE_REMOVING && state != STATE_RESTARTING;
}
@Override
public String toString() {
- return action.type
- + ' '
- + (action.isRemoveAction ? "remove" : "download")
- + ' '
- + TaskState.getStateString(state)
- + ' '
- + TaskState.getStateString(targetState);
+ return id + ' ' + DownloadState.getStateString(state);
}
- public boolean canStart() {
- return state == STATE_QUEUED;
+ public boolean start() {
+ if (state != STATE_QUEUED) {
+ return false;
+ }
+ startDownloadThread(actionQueue.peek());
+ setState(STATE_DOWNLOADING);
+ return true;
}
- public void start() {
+ public void setStopFlags(int flags) {
+ updateStopFlags(flags, flags);
+ }
+
+ public void clearStopFlags(int flags) {
+ updateStopFlags(flags, 0);
+ }
+
+ public void updateStopFlags(int flags, int values) {
+ stopFlags = (values & flags) | (stopFlags & ~flags);
+ if (stopFlags != 0) {
+ if (state == STATE_DOWNLOADING) {
+ stopDownloadThread();
+ } else if (state == STATE_QUEUED) {
+ setState(STATE_STOPPED);
+ }
+ } else if (state == STATE_STOPPED) {
+ startOrQueue(/* restart= */ false);
+ }
+ }
+
+ private void initialize(boolean restart) {
+ DownloadAction action = actionQueue.peek();
+ if (action.isRemoveAction) {
+ if (!downloadManager.released) {
+ startDownloadThread(action);
+ }
+ setState(actionQueue.size() == 1 ? STATE_REMOVING : STATE_RESTARTING);
+ } else if (stopFlags != 0) {
+ setState(STATE_STOPPED);
+ } else {
+ startOrQueue(restart);
+ }
+ }
+
+ private void startOrQueue(boolean restart) {
+ // Set to queued state but don't notify listeners until we make sure we can't start now.
+ state = STATE_QUEUED;
+ if (restart) {
+ start();
+ } else {
+ downloadManager.maybeStartDownload(this);
+ }
if (state == STATE_QUEUED) {
- state = STATE_STARTED;
- targetState = STATE_COMPLETED;
- downloadManager.onTaskStateChange(this);
- downloader = downloaderFactory.createDownloader(action);
- thread = new Thread(this);
- thread.start();
+ downloadManager.onDownloadStateChange(this);
}
}
- public void cancel() {
- if (state == STATE_STARTED) {
- stopDownloadThread(STATE_CANCELED);
- } else if (state == STATE_QUEUED) {
- state = STATE_CANCELED;
- downloadManager.handler.post(() -> downloadManager.onTaskStateChange(this));
- }
+ private void setState(@DownloadState.State int newState) {
+ state = newState;
+ downloadManager.onDownloadStateChange(this);
}
- public void stop() {
- if (state == STATE_STARTED && targetState == STATE_COMPLETED) {
- stopDownloadThread(STATE_QUEUED);
- }
+ private void startDownloadThread(DownloadAction action) {
+ downloader = downloaderFactory.createDownloader(action);
+ downloadThread =
+ new DownloadThread(
+ this, downloader, action.isRemoveAction, minRetryCount, downloadManager.handler);
}
- // Internal methods running on the main thread.
-
- private void stopDownloadThread(@TargetState int targetState) {
- this.targetState = targetState;
- Assertions.checkNotNull(downloader).cancel();
- Assertions.checkNotNull(thread).interrupt();
+ private void stopDownloadThread() {
+ Assertions.checkNotNull(downloadThread).cancel();
}
private void onDownloadThreadStopped(@Nullable Throwable finalError) {
- @TaskState.State int finalState = targetState;
- if (targetState == STATE_COMPLETED && finalError != null) {
- finalState = STATE_FAILED;
- } else {
- finalError = null;
+ failureReason = FAILURE_REASON_NONE;
+ if (!downloadThread.isCanceled) {
+ if (finalError != null && state != STATE_REMOVING && state != STATE_RESTARTING) {
+ failureReason = FAILURE_REASON_UNKNOWN;
+ setState(STATE_FAILED);
+ return;
+ }
+ if (actionQueue.size() == 1) {
+ if (state == STATE_REMOVING) {
+ setState(STATE_REMOVED);
+ } else {
+ Assertions.checkState(state == STATE_DOWNLOADING);
+ setState(STATE_COMPLETED);
+ }
+ return;
+ }
+ actionQueue.remove();
}
- state = finalState;
- error = finalError;
- downloadManager.onTaskStateChange(this);
+ initialize(/* restart= */ state == STATE_DOWNLOADING);
+ }
+ }
+
+ private static class DownloadThread implements Runnable {
+
+ private final Download download;
+ private final Downloader downloader;
+ private final boolean remove;
+ private final int minRetryCount;
+ private final Handler callbackHandler;
+ private final Thread thread;
+ private volatile boolean isCanceled;
+
+ private DownloadThread(
+ Download download,
+ Downloader downloader,
+ boolean remove,
+ int minRetryCount,
+ Handler callbackHandler) {
+ this.download = download;
+ this.downloader = downloader;
+ this.remove = remove;
+ this.minRetryCount = minRetryCount;
+ this.callbackHandler = callbackHandler;
+ thread = new Thread(this);
+ thread.start();
+ }
+
+ public void cancel() {
+ isCanceled = true;
+ downloader.cancel();
+ thread.interrupt();
}
// Methods running on download thread.
@Override
public void run() {
- logd("Task is started", this);
+ logd("Download is started", download);
Throwable error = null;
try {
- if (action.isRemoveAction) {
+ if (remove) {
downloader.remove();
} else {
int errorCount = 0;
long errorPosition = C.LENGTH_UNSET;
- while (targetState == STATE_COMPLETED) {
+ while (!isCanceled) {
try {
downloader.download();
break;
} catch (IOException e) {
- if (targetState == STATE_COMPLETED) {
+ if (!isCanceled) {
long downloadedBytes = downloader.getDownloadedBytes();
if (downloadedBytes != errorPosition) {
- logd("Reset error count. downloadedBytes = " + downloadedBytes, this);
+ logd("Reset error count. downloadedBytes = " + downloadedBytes, download);
errorPosition = downloadedBytes;
errorCount = 0;
}
if (++errorCount > minRetryCount) {
throw e;
}
- logd("Download error. Retry " + errorCount, this);
+ logd("Download error. Retry " + errorCount, download);
Thread.sleep(getRetryDelayMillis(errorCount));
}
}
@@ -702,7 +762,7 @@ public final class DownloadManager {
error = e;
}
final Throwable finalError = error;
- downloadManager.handler.post(() -> onDownloadThreadStopped(finalError));
+ callbackHandler.post(() -> download.onDownloadThreadStopped(isCanceled ? null : finalError));
}
private int getRetryDelayMillis(int errorCount) {
@@ -710,4 +770,19 @@ public final class DownloadManager {
}
}
+ private class RequirementListener implements RequirementsWatcher.Listener {
+ @Override
+ public void requirementsMet(RequirementsWatcher requirementsWatcher) {
+ startDownloads();
+ notifyListenersRequirementsStateChange(0);
+ }
+
+ @Override
+ public void requirementsNotMet(
+ RequirementsWatcher requirementsWatcher,
+ @Requirements.RequirementFlags int notMetRequirements) {
+ stopDownloads();
+ notifyListenersRequirementsStateChange(notMetRequirements);
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
index cfca8ede79..d424ed5ef0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java
@@ -24,10 +24,9 @@ import android.os.IBinder;
import android.os.Looper;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
-import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
import com.google.android.exoplayer2.scheduler.Requirements;
-import com.google.android.exoplayer2.scheduler.RequirementsWatcher;
import com.google.android.exoplayer2.scheduler.Scheduler;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.NotificationUtil;
import com.google.android.exoplayer2.util.Util;
@@ -44,10 +43,6 @@ public abstract class DownloadService extends Service {
/** Starts a download service, adding a new {@link DownloadAction} to be executed. */
public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD";
- /** Reloads the download requirements. */
- public static final String ACTION_RELOAD_REQUIREMENTS =
- "com.google.android.exoplayer.downloadService.action.RELOAD_REQUIREMENTS";
-
/** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */
private static final String ACTION_RESTART =
"com.google.android.exoplayer.downloadService.action.RESTART";
@@ -71,20 +66,16 @@ public abstract class DownloadService extends Service {
private static final String TAG = "DownloadService";
private static final boolean DEBUG = false;
- // Keep the requirements helper for each DownloadService as long as there are tasks (and the
- // process is running). This allows tasks to resume when there's no scheduler. It may also allow
- // tasks the resume more quickly than when relying on the scheduler alone.
- private static final HashMap, RequirementsHelper>
- requirementsHelpers = new HashMap<>();
- private static final Requirements DEFAULT_REQUIREMENTS =
- new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
+ // Keep DownloadManagerListeners for each DownloadService as long as there are downloads (and the
+ // process is running). This allows DownloadService to restart when there's no scheduler.
+ private static final HashMap, DownloadManagerHelper>
+ downloadManagerListeners = new HashMap<>();
private final @Nullable ForegroundNotificationUpdater foregroundNotificationUpdater;
private final @Nullable String channelId;
private final @StringRes int channelName;
private DownloadManager downloadManager;
- private DownloadManagerListener downloadManagerListener;
private int lastStartId;
private boolean startedInForeground;
private boolean taskRemoved;
@@ -99,7 +90,7 @@ public abstract class DownloadService extends Service {
* If {@code foregroundNotificationId} isn't {@link #FOREGROUND_NOTIFICATION_ID_NONE} (value
* {@value #FOREGROUND_NOTIFICATION_ID_NONE}) the service runs in the foreground with {@link
* #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. In that case {@link
- * #getForegroundNotification(TaskState[])} should be overridden in the subclass.
+ * #getForegroundNotification(DownloadState[])} should be overridden in the subclass.
*
* @param foregroundNotificationId The notification id for the foreground notification, or {@link
* #FOREGROUND_NOTIFICATION_ID_NONE} (value {@value #FOREGROUND_NOTIFICATION_ID_NONE})
@@ -110,7 +101,7 @@ public abstract class DownloadService extends Service {
/**
* Creates a DownloadService which will run in the foreground. {@link
- * #getForegroundNotification(TaskState[])} should be overridden in the subclass.
+ * #getForegroundNotification(DownloadState[])} should be overridden in the subclass.
*
* @param foregroundNotificationId The notification id for the foreground notification, must not
* be 0.
@@ -128,7 +119,7 @@ public abstract class DownloadService extends Service {
/**
* Creates a DownloadService which will run in the foreground. {@link
- * #getForegroundNotification(TaskState[])} should be overridden in the subclass.
+ * #getForegroundNotification(DownloadState[])} should be overridden in the subclass.
*
* @param foregroundNotificationId The notification id for the foreground notification. Must not
* be 0.
@@ -228,9 +219,16 @@ public abstract class DownloadService extends Service {
NotificationUtil.createNotificationChannel(
this, channelId, channelName, NotificationUtil.IMPORTANCE_LOW);
}
- downloadManager = getDownloadManager();
- downloadManagerListener = new DownloadManagerListener();
- downloadManager.addListener(downloadManagerListener);
+ Class extends DownloadService> clazz = getClass();
+ DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz);
+ if (downloadManagerHelper == null) {
+ downloadManagerHelper =
+ new DownloadManagerHelper(
+ getApplicationContext(), getDownloadManager(), getScheduler(), clazz);
+ downloadManagerListeners.put(clazz, downloadManagerHelper);
+ }
+ downloadManager = downloadManagerHelper.downloadManager;
+ downloadManagerHelper.attachService(this);
}
@Override
@@ -265,22 +263,11 @@ public abstract class DownloadService extends Service {
}
}
break;
- case ACTION_RELOAD_REQUIREMENTS:
- stopWatchingRequirements();
- break;
default:
Log.e(TAG, "Ignoring unrecognized action: " + intentAction);
break;
}
- Requirements requirements = getRequirements();
- if (requirements.checkRequirements(this)) {
- downloadManager.startDownloads();
- } else {
- downloadManager.stopDownloads();
- }
- maybeStartWatchingRequirements(requirements);
-
if (downloadManager.isIdle()) {
stop();
}
@@ -296,11 +283,12 @@ public abstract class DownloadService extends Service {
@Override
public void onDestroy() {
logd("onDestroy");
+ DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(getClass());
+ boolean unschedule = downloadManager.getDownloadCount() <= 0;
+ downloadManagerHelper.detachService(this, unschedule);
if (foregroundNotificationUpdater != null) {
foregroundNotificationUpdater.stopPeriodicUpdates();
}
- downloadManager.removeListener(downloadManagerListener);
- maybeStopWatchingRequirements();
}
/** DownloadService isn't designed to be bound. */
@@ -312,9 +300,7 @@ public abstract class DownloadService extends Service {
/**
* Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the
- * life cycle of the service. The service will call {@link DownloadManager#startDownloads()} and
- * {@link DownloadManager#stopDownloads} as necessary when requirements returned by {@link
- * #getRequirements()} are met or stop being met.
+ * life cycle of the process.
*/
protected abstract DownloadManager getDownloadManager();
@@ -325,71 +311,47 @@ public abstract class DownloadService extends Service {
*/
protected abstract @Nullable Scheduler getScheduler();
- /**
- * Returns requirements for downloads to take place. By default the only requirement is that the
- * device has network connectivity.
- */
- protected Requirements getRequirements() {
- return DEFAULT_REQUIREMENTS;
- }
-
/**
* Should be overridden in the subclass if the service will be run in the foreground.
*
*
Returns a notification to be displayed when this service running in the foreground.
*
- *
This method is called when there is a task state change and periodically while there are
- * active tasks. The periodic update interval can be set using {@link #DownloadService(int,
+ *
This method is called when there is a download state change and periodically while there are
+ * active downloads. The periodic update interval can be set using {@link #DownloadService(int,
* long)}.
*
*
On API level 26 and above, this method may also be called just before the service stops,
- * with an empty {@code taskStates} array. The returned notification is used to satisfy system
+ * with an empty {@code downloadStates} array. The returned notification is used to satisfy system
* requirements for foreground services.
*
- * @param taskStates The states of all current tasks.
+ * @param downloadStates The states of all current downloads.
* @return The foreground notification to display.
*/
- protected Notification getForegroundNotification(TaskState[] taskStates) {
+ protected Notification getForegroundNotification(DownloadState[] downloadStates) {
throw new IllegalStateException(
getClass().getName()
+ " is started in the foreground but getForegroundNotification() is not implemented.");
}
/**
- * Called when the state of a task changes.
+ * Called when the state of a download changes.
*
- * @param taskState The state of the task.
+ * @param downloadState The state of the download.
*/
- protected void onTaskStateChanged(TaskState taskState) {
+ protected void onDownloadStateChanged(DownloadState downloadState) {
// Do nothing.
}
- private void maybeStartWatchingRequirements(Requirements requirements) {
- if (downloadManager.getDownloadCount() == 0) {
- return;
- }
- Class extends DownloadService> clazz = getClass();
- RequirementsHelper requirementsHelper = requirementsHelpers.get(clazz);
- if (requirementsHelper == null) {
- requirementsHelper = new RequirementsHelper(this, requirements, getScheduler(), clazz);
- requirementsHelpers.put(clazz, requirementsHelper);
- requirementsHelper.start();
- logd("started watching requirements");
- }
- }
-
- private void maybeStopWatchingRequirements() {
- if (downloadManager.getDownloadCount() > 0) {
- return;
- }
- stopWatchingRequirements();
- }
-
- private void stopWatchingRequirements() {
- RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass());
- if (requirementsHelper != null) {
- requirementsHelper.stop();
- logd("stopped watching requirements");
+ private void notifyDownloadStateChange(DownloadState downloadState) {
+ onDownloadStateChanged(downloadState);
+ if (foregroundNotificationUpdater != null) {
+ if (downloadState.state == DownloadState.STATE_DOWNLOADING
+ || downloadState.state == DownloadState.STATE_REMOVING
+ || downloadState.state == DownloadState.STATE_RESTARTING) {
+ foregroundNotificationUpdater.startPeriodicUpdates();
+ } else {
+ foregroundNotificationUpdater.update();
+ }
}
}
@@ -421,30 +383,6 @@ public abstract class DownloadService extends Service {
return new Intent(context, clazz).setAction(action);
}
- private final class DownloadManagerListener implements DownloadManager.Listener {
- @Override
- public void onInitialized(DownloadManager downloadManager) {
- maybeStartWatchingRequirements(getRequirements());
- }
-
- @Override
- public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
- DownloadService.this.onTaskStateChanged(taskState);
- if (foregroundNotificationUpdater != null) {
- if (taskState.state == TaskState.STATE_STARTED) {
- foregroundNotificationUpdater.startPeriodicUpdates();
- } else {
- foregroundNotificationUpdater.update();
- }
- }
- }
-
- @Override
- public final void onIdle(DownloadManager downloadManager) {
- stop();
- }
- }
-
private final class ForegroundNotificationUpdater implements Runnable {
private final int notificationId;
@@ -471,8 +409,8 @@ public abstract class DownloadService extends Service {
}
public void update() {
- TaskState[] taskStates = downloadManager.getAllTaskStates();
- startForeground(notificationId, getForegroundNotification(taskStates));
+ DownloadState[] downloadStates = downloadManager.getAllDownloadStates();
+ startForeground(notificationId, getForegroundNotification(downloadStates));
notificationDisplayed = true;
if (periodicUpdatesStarted) {
handler.removeCallbacks(this);
@@ -492,58 +430,87 @@ public abstract class DownloadService extends Service {
}
}
- private static final class RequirementsHelper implements RequirementsWatcher.Listener {
+ private static final class DownloadManagerHelper implements DownloadManager.Listener {
private final Context context;
- private final Requirements requirements;
- private final @Nullable Scheduler scheduler;
+ private final DownloadManager downloadManager;
+ @Nullable private final Scheduler scheduler;
private final Class extends DownloadService> serviceClass;
- private final RequirementsWatcher requirementsWatcher;
+ @Nullable private DownloadService downloadService;
- private RequirementsHelper(
+ private DownloadManagerHelper(
Context context,
- Requirements requirements,
+ DownloadManager downloadManager,
@Nullable Scheduler scheduler,
Class extends DownloadService> serviceClass) {
this.context = context;
- this.requirements = requirements;
+ this.downloadManager = downloadManager;
this.scheduler = scheduler;
this.serviceClass = serviceClass;
- requirementsWatcher = new RequirementsWatcher(context, this, requirements);
- }
-
- public void start() {
- requirementsWatcher.start();
- }
-
- public void stop() {
- requirementsWatcher.stop();
+ downloadManager.addListener(this);
if (scheduler != null) {
+ Requirements requirements = downloadManager.getRequirements();
+ setSchedulerEnabled(/* enabled= */ !requirements.checkRequirements(context), requirements);
+ }
+ }
+
+ public void attachService(DownloadService downloadService) {
+ Assertions.checkState(this.downloadService == null);
+ this.downloadService = downloadService;
+ }
+
+ public void detachService(DownloadService downloadService, boolean unschedule) {
+ Assertions.checkState(this.downloadService == downloadService);
+ this.downloadService = null;
+ if (unschedule) {
scheduler.cancel();
}
}
@Override
- public void requirementsMet(RequirementsWatcher requirementsWatcher) {
- try {
- notifyService();
- } catch (Exception e) {
- /* If we can't notify the service, don't stop the scheduler. */
- return;
- }
- if (scheduler != null) {
- scheduler.cancel();
+ public void onInitialized(DownloadManager downloadManager) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onDownloadStateChanged(
+ DownloadManager downloadManager, DownloadState downloadState) {
+ if (downloadService != null) {
+ downloadService.notifyDownloadStateChange(downloadState);
}
}
@Override
- public void requirementsNotMet(RequirementsWatcher requirementsWatcher) {
- try {
- notifyService();
- } catch (Exception e) {
- /* Do nothing. The service isn't running anyway. */
+ public final void onIdle(DownloadManager downloadManager) {
+ if (downloadService != null) {
+ downloadService.stop();
+ }
+ }
+
+ @Override
+ public void onRequirementsStateChanged(
+ DownloadManager downloadManager,
+ Requirements requirements,
+ @Requirements.RequirementFlags int notMetRequirements) {
+ boolean requirementsMet = notMetRequirements == 0;
+ if (downloadService == null && requirementsMet) {
+ try {
+ Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);
+ context.startService(intent);
+ } catch (IllegalStateException e) {
+ /* startService fails if the app is in the background then don't stop the scheduler. */
+ return;
+ }
}
if (scheduler != null) {
+ setSchedulerEnabled(/* enabled= */ !requirementsMet, requirements);
+ }
+ }
+
+ private void setSchedulerEnabled(boolean enabled, Requirements requirements) {
+ if (!enabled) {
+ scheduler.cancel();
+ } else {
String servicePackage = context.getPackageName();
boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART);
if (!success) {
@@ -551,15 +518,5 @@ public abstract class DownloadService extends Service {
}
}
}
-
- private void notifyService() throws Exception {
- Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT);
- try {
- context.startService(intent);
- } catch (IllegalStateException e) {
- /* startService will fail if the app is in the background and the service isn't running. */
- throw new Exception(e);
- }
- }
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java
new file mode 100644
index 0000000000..7bbd078822
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+import android.net.Uri;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Represents state of a download. */
+public final class DownloadState {
+
+ /**
+ * Download states. One of {@link #STATE_QUEUED}, {@link #STATE_STOPPED}, {@link
+ * #STATE_DOWNLOADING}, {@link #STATE_COMPLETED}, {@link #STATE_FAILED}, {@link #STATE_REMOVING},
+ * {@link #STATE_REMOVED} or {@link #STATE_RESTARTING}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATE_QUEUED,
+ STATE_STOPPED,
+ STATE_DOWNLOADING,
+ STATE_COMPLETED,
+ STATE_FAILED,
+ STATE_REMOVING,
+ STATE_REMOVED,
+ STATE_RESTARTING
+ })
+ public @interface State {}
+ /** The download is waiting to be started. */
+ public static final int STATE_QUEUED = 0;
+ /** The download is stopped. */
+ public static final int STATE_STOPPED = 1;
+ /** The download is currently started. */
+ public static final int STATE_DOWNLOADING = 2;
+ /** The download completed. */
+ public static final int STATE_COMPLETED = 3;
+ /** The download failed. */
+ public static final int STATE_FAILED = 4;
+ /** The download is being removed. */
+ public static final int STATE_REMOVING = 5;
+ /** The download is removed. */
+ public static final int STATE_REMOVED = 6;
+ /** The download will restart after all downloaded data is removed. */
+ public static final int STATE_RESTARTING = 7;
+
+ /** Failure reasons. Either {@link #FAILURE_REASON_NONE} or {@link #FAILURE_REASON_UNKNOWN}. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FAILURE_REASON_NONE, FAILURE_REASON_UNKNOWN})
+ public @interface FailureReason {}
+ /** The download isn't failed. */
+ public static final int FAILURE_REASON_NONE = 0;
+ /** The download is failed because of unknown reason. */
+ public static final int FAILURE_REASON_UNKNOWN = 1;
+
+ /**
+ * Download stop flags. Possible flag values are {@link #STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY} and
+ * {@link #STOP_FLAG_STOPPED}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY, STOP_FLAG_STOPPED})
+ public @interface StopFlags {}
+ /** Download can't be started as the manager isn't ready. */
+ public static final int STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY = 1;
+ /** All downloads are stopped by the application. */
+ public static final int STOP_FLAG_STOPPED = 1 << 1;
+
+ /** Returns the state string for the given state value. */
+ public static String getStateString(@State int state) {
+ switch (state) {
+ case STATE_QUEUED:
+ return "QUEUED";
+ case STATE_STOPPED:
+ return "STOPPED";
+ case STATE_DOWNLOADING:
+ return "DOWNLOADING";
+ case STATE_COMPLETED:
+ return "COMPLETED";
+ case STATE_FAILED:
+ return "FAILED";
+ case STATE_REMOVING:
+ return "REMOVING";
+ case STATE_REMOVED:
+ return "REMOVED";
+ case STATE_RESTARTING:
+ return "RESTARTING";
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ /** Returns the failure string for the given failure reason value. */
+ public static String getFailureString(@FailureReason int failureReason) {
+ switch (failureReason) {
+ case FAILURE_REASON_NONE:
+ return "NO_REASON";
+ case FAILURE_REASON_UNKNOWN:
+ return "UNKNOWN_REASON";
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ /** The unique content id. */
+ public final String id;
+ /** The type of the content. */
+ public final String type;
+ /** The Uri of the content. */
+ public final Uri uri;
+ /** A custom key for cache indexing. */
+ @Nullable public final String cacheKey;
+ /** The state of the download. */
+ @State public final int state;
+ /** The estimated download percentage, or {@link C#PERCENTAGE_UNSET} if unavailable. */
+ public final float downloadPercentage;
+ /** The total number of downloaded bytes. */
+ public final long downloadedBytes;
+ /** The total size of the media, or {@link C#LENGTH_UNSET} if unknown. */
+ public final long totalBytes;
+ /** The first time when download entry is created. */
+ public final long startTimeMs;
+ /** The last update time. */
+ public final long updateTimeMs;
+ /** Keys of streams to be downloaded. If empty, all streams will be downloaded. */
+ public final StreamKey[] streamKeys;
+ /** Optional custom data. */
+ public final byte[] customMetadata;
+ /**
+ * If {@link #state} is {@link #STATE_FAILED} then this is the cause, otherwise {@link
+ * #FAILURE_REASON_NONE}.
+ */
+ @FailureReason public final int failureReason;
+ /** Download stop flags. These flags stop downloading any content. */
+ public final int stopFlags;
+
+ /* package */ DownloadState(
+ String id,
+ String type,
+ Uri uri,
+ @Nullable String cacheKey,
+ @State int state,
+ float downloadPercentage,
+ long downloadedBytes,
+ long totalBytes,
+ @FailureReason int failureReason,
+ @StopFlags int stopFlags,
+ long startTimeMs,
+ long updateTimeMs,
+ StreamKey[] streamKeys,
+ byte[] customMetadata) {
+ this.stopFlags = stopFlags;
+ Assertions.checkState(
+ failureReason == FAILURE_REASON_NONE ? state != STATE_FAILED : state == STATE_FAILED);
+ // TODO enable this when we start changing state immediately
+ // Assertions.checkState(stopFlags == 0 || (state != STATE_DOWNLOADING && state !=
+ // STATE_QUEUED));
+ this.id = id;
+ this.type = type;
+ this.uri = uri;
+ this.cacheKey = cacheKey;
+ this.streamKeys = streamKeys;
+ this.customMetadata = customMetadata;
+ this.state = state;
+ this.downloadPercentage = downloadPercentage;
+ this.downloadedBytes = downloadedBytes;
+ this.totalBytes = totalBytes;
+ this.failureReason = failureReason;
+ this.startTimeMs = startTimeMs;
+ this.updateTimeMs = updateTimeMs;
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadStateCursor.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadStateCursor.java
new file mode 100644
index 0000000000..680976c77b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadStateCursor.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.offline;
+
+/** Provides random read-write access to the result set returned by a database query. */
+interface DownloadStateCursor {
+
+ /** Returns the DownloadState at the current position. */
+ DownloadState getDownloadState();
+
+ /** Returns the numbers of DownloadStates in the cursor. */
+ int getCount();
+
+ /**
+ * Returns the current position of the cursor in the DownloadState set. The value is zero-based.
+ * When the DownloadState set is first returned the cursor will be at positon -1, which is before
+ * the first DownloadState. After the last DownloadState is returned another call to next() will
+ * leave the cursor past the last entry, at a position of count().
+ *
+ * @return the current cursor position.
+ */
+ int getPosition();
+
+ /**
+ * Move the cursor to an absolute position. The valid range of values is -1 <= position <=
+ * count.
+ *
+ *
This method will return true if the request destination was reachable, otherwise, it returns
+ * false.
+ *
+ * @param position the zero-based position to move to.
+ * @return whether the requested move fully succeeded.
+ */
+ boolean moveToPosition(int position);
+
+ /**
+ * Move the cursor to the first DownloadState.
+ *
+ *
This method will return false if the cursor is empty.
+ *
+ * @return whether the move succeeded.
+ */
+ default boolean moveToFirst() {
+ return moveToPosition(0);
+ }
+
+ /**
+ * Move the cursor to the last DownloadState.
+ *
+ *
This method will return false if the cursor is empty.
+ *
+ * @return whether the move succeeded.
+ */
+ default boolean moveToLast() {
+ return moveToPosition(getCount() - 1);
+ }
+
+ /**
+ * Move the cursor to the next DownloadState.
+ *
+ *
This method will return false if the cursor is already past the last entry in the result
+ * set.
+ *
+ * @return whether the move succeeded.
+ */
+ default boolean moveToNext() {
+ return moveToPosition(getPosition() + 1);
+ }
+
+ /**
+ * Move the cursor to the previous DownloadState.
+ *
+ *
This method will return false if the cursor is already before the first entry in the result
+ * set.
+ *
+ * @return whether the move succeeded.
+ */
+ default boolean moveToPrevious() {
+ return moveToPosition(getPosition() - 1);
+ }
+
+ /** Returns whether the cursor is pointing to the first DownloadState. */
+ default boolean isFirst() {
+ return getPosition() == 0 && getCount() != 0;
+ }
+
+ /** Returns whether the cursor is pointing to the last DownloadState. */
+ default boolean isLast() {
+ int count = getCount();
+ return getPosition() == (count - 1) && count != 0;
+ }
+
+ /** Returns whether the cursor is pointing to the position before the first DownloadState. */
+ default boolean isBeforeFirst() {
+ if (getCount() == 0) {
+ return true;
+ }
+ return getPosition() == -1;
+ }
+
+ /** Returns whether the cursor is pointing to the position after the last DownloadState. */
+ default boolean isAfterLast() {
+ if (getCount() == 0) {
+ return true;
+ }
+ return getPosition() == getCount();
+ }
+
+ /** Closes the Cursor, releasing all of its resources and making it completely invalid. */
+ void close();
+
+ /** Returns whether the cursor is closed */
+ boolean isClosed();
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java
index 74b918c06d..59a11934b1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java
@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
import com.google.android.exoplayer2.upstream.PriorityDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.Cache;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
import com.google.android.exoplayer2.upstream.cache.CacheDataSinkFactory;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
@@ -108,16 +109,18 @@ public final class DownloaderConstructorHelper {
cacheReadDataSourceFactory != null
? cacheReadDataSourceFactory
: new FileDataSourceFactory();
- DataSink.Factory writeDataSinkFactory =
- cacheWriteDataSinkFactory != null
- ? cacheWriteDataSinkFactory
- : new CacheDataSinkFactory(cache, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE);
+ if (cacheWriteDataSinkFactory == null) {
+ CacheDataSinkFactory factory =
+ new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE);
+ factory.experimental_setRespectCacheFragmentationFlag(true);
+ cacheWriteDataSinkFactory = factory;
+ }
onlineCacheDataSourceFactory =
new CacheDataSourceFactory(
cache,
upstreamFactory,
readDataSourceFactory,
- writeDataSinkFactory,
+ cacheWriteDataSinkFactory,
CacheDataSource.FLAG_BLOCK_ON_CACHE,
/* eventListener= */ null,
cacheKeyFactory);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java
index c32cdf7126..c25e5099cf 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/FilteringManifestParser.java
@@ -16,22 +16,27 @@
package com.google.android.exoplayer2.offline;
import android.net.Uri;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
-/** A manifest parser that includes only the streams identified by the given stream keys. */
+/**
+ * A manifest parser that includes only the streams identified by the given stream keys.
+ *
+ * @param The {@link FilterableManifest} type.
+ */
public final class FilteringManifestParser> implements Parser {
- private final Parser parser;
- private final List streamKeys;
+ private final Parser extends T> parser;
+ @Nullable private final List streamKeys;
/**
* @param parser A parser for the manifest that will be filtered.
* @param streamKeys The stream keys. If null or empty then filtering will not occur.
*/
- public FilteringManifestParser(Parser parser, List streamKeys) {
+ public FilteringManifestParser(Parser extends T> parser, @Nullable List streamKeys) {
this.parser = parser;
this.streamKeys = streamKeys;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java
index 70587694c4..2ec14368ca 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java
@@ -17,19 +17,35 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri;
import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import java.util.Collections;
-import java.util.List;
/** A {@link DownloadHelper} for progressive streams. */
public final class ProgressiveDownloadHelper extends DownloadHelper {
+ /**
+ * Creates download helper for progressive streams.
+ *
+ * @param uri The stream {@link Uri}.
+ */
public ProgressiveDownloadHelper(Uri uri) {
- this(uri, null);
+ this(uri, /* cacheKey= */ null);
}
- public ProgressiveDownloadHelper(Uri uri, @Nullable String customCacheKey) {
- super(DownloadAction.TYPE_PROGRESSIVE, uri, customCacheKey);
+ /**
+ * Creates download helper for progressive streams.
+ *
+ * @param uri The stream {@link Uri}.
+ * @param cacheKey An optional cache key.
+ */
+ public ProgressiveDownloadHelper(Uri uri, @Nullable String cacheKey) {
+ super(
+ DownloadAction.TYPE_PROGRESSIVE,
+ uri,
+ cacheKey,
+ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
+ (handler, videoListener, audioListener, metadata, text, drm) -> new Renderer[0],
+ /* drmSessionManager= */ null);
}
@Override
@@ -43,7 +59,8 @@ public final class ProgressiveDownloadHelper extends DownloadHelper {
}
@Override
- protected List toStreamKeys(List trackKeys) {
- return Collections.emptyList();
+ protected StreamKey toStreamKey(
+ int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
+ return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java
index 41f0944b75..25b4e07bcd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java
@@ -53,7 +53,11 @@ public final class ProgressiveDownloader implements Downloader {
Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) {
this.dataSpec =
new DataSpec(
- uri, /* absoluteStreamPosition= */ 0, C.LENGTH_UNSET, customCacheKey, /* flags= */ 0);
+ uri,
+ /* absoluteStreamPosition= */ 0,
+ C.LENGTH_UNSET,
+ customCacheKey,
+ /* flags= */ DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);
this.cache = constructorHelper.getCache();
this.dataSource = constructorHelper.createCacheDataSource();
this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java
index 838073cd99..1caeaca61e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java
@@ -19,8 +19,11 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
- * Identifies a given track by the index of the containing period, the index of the containing group
- * within the period, and the index of the track within the group.
+ * A key for a subset of media which can be separately loaded (a "stream").
+ *
+ * The stream key consists of a period index, a group index within the period and a track index
+ * within the group. The interpretation of these indices depends on the type of media for which the
+ * stream key is used.
*/
public final class StreamKey implements Comparable {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java
deleted file mode 100644
index f6a411c3a1..0000000000
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.offline;
-
-/**
- * Identifies a given track by the index of the containing period, the index of the containing group
- * within the period, and the index of the track within the group.
- */
-public final class TrackKey {
-
- /** The period index. */
- public final int periodIndex;
- /** The group index. */
- public final int groupIndex;
- /** The track index. */
- public final int trackIndex;
-
- /**
- * @param periodIndex The period index.
- * @param groupIndex The group index.
- * @param trackIndex The track index.
- */
- public TrackKey(int periodIndex, int groupIndex, int trackIndex) {
- this.periodIndex = periodIndex;
- this.groupIndex = groupIndex;
- this.trackIndex = trackIndex;
- }
-}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
index ed06d3745a..b8272dc036 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
@@ -130,7 +130,7 @@ public final class PlatformScheduler implements Scheduler {
PersistableBundle extras = new PersistableBundle();
extras.putString(KEY_SERVICE_ACTION, serviceAction);
extras.putString(KEY_SERVICE_PACKAGE, servicePackage);
- extras.putInt(KEY_REQUIREMENTS, requirements.getRequirementsData());
+ extras.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
builder.setExtras(extras);
return builder.build();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java
index 5acd31ee0d..77630a4543 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java
@@ -25,6 +25,7 @@ import android.net.NetworkInfo;
import android.os.BatteryManager;
import android.os.PowerManager;
import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
@@ -50,22 +51,49 @@ public final class Requirements {
NETWORK_TYPE_METERED,
})
public @interface NetworkType {}
+
+ /**
+ * Requirement flags.
+ *
+ * Combination of the following values is possible:
+ *
+ *
+ * - Only one of {@link #NETWORK_TYPE_ANY}, {@link #NETWORK_TYPE_UNMETERED}, {@link
+ * #NETWORK_TYPE_NOT_ROAMING} or {@link #NETWORK_TYPE_METERED}.
+ *
- {@link #DEVICE_IDLE}
+ *
- {@link #DEVICE_CHARGING}
+ *
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ NETWORK_TYPE_ANY,
+ NETWORK_TYPE_UNMETERED,
+ NETWORK_TYPE_NOT_ROAMING,
+ NETWORK_TYPE_METERED,
+ DEVICE_IDLE,
+ DEVICE_CHARGING
+ })
+ public @interface RequirementFlags {}
+
/** This job doesn't require network connectivity. */
public static final int NETWORK_TYPE_NONE = 0;
/** This job requires network connectivity. */
public static final int NETWORK_TYPE_ANY = 1;
/** This job requires network connectivity that is unmetered. */
- public static final int NETWORK_TYPE_UNMETERED = 2;
+ public static final int NETWORK_TYPE_UNMETERED = 1 << 1;
/** This job requires network connectivity that is not roaming. */
- public static final int NETWORK_TYPE_NOT_ROAMING = 3;
+ public static final int NETWORK_TYPE_NOT_ROAMING = 1 << 2;
/** This job requires metered connectivity such as most cellular data networks. */
- public static final int NETWORK_TYPE_METERED = 4;
+ public static final int NETWORK_TYPE_METERED = 1 << 3;
/** This job requires the device to be idle. */
- private static final int DEVICE_IDLE = 8;
+ public static final int DEVICE_IDLE = 1 << 4;
/** This job requires the device to be charging. */
- private static final int DEVICE_CHARGING = 16;
+ public static final int DEVICE_CHARGING = 1 << 5;
- private static final int NETWORK_TYPE_MASK = 7;
+ private static final int NETWORK_TYPE_MASK = 0b1111;
private static final String TAG = "Requirements";
@@ -86,7 +114,7 @@ public final class Requirements {
}
}
- private final int requirements;
+ @RequirementFlags private final int requirements;
/**
* @param networkType Required network type.
@@ -97,9 +125,12 @@ public final class Requirements {
this(networkType | (charging ? DEVICE_CHARGING : 0) | (idle ? DEVICE_IDLE : 0));
}
- /** @param requirementsData The value returned by {@link #getRequirementsData()}. */
- public Requirements(int requirementsData) {
- this.requirements = requirementsData;
+ /** @param requirements A combination of requirement flags. */
+ public Requirements(@RequirementFlags int requirements) {
+ this.requirements = requirements;
+ int networkType = getRequiredNetworkType();
+ // Check if only one network type is specified.
+ Assertions.checkState((networkType & (networkType - 1)) == 0);
}
/** Returns required network type. */
@@ -121,15 +152,28 @@ public final class Requirements {
* Returns whether the requirements are met.
*
* @param context Any context.
+ * @return Whether the requirements are met.
*/
public boolean checkRequirements(Context context) {
- return checkNetworkRequirements(context)
- && checkChargingRequirement(context)
- && checkIdleRequirement(context);
+ return getNotMetRequirements(context) == 0;
}
- /** Returns the encoded requirements data which can be used with {@link #Requirements(int)}. */
- public int getRequirementsData() {
+ /**
+ * Returns {@link RequirementFlags} that are not met, or 0.
+ *
+ * @param context Any context.
+ * @return RequirementFlags that are not met, or 0.
+ */
+ @RequirementFlags
+ public int getNotMetRequirements(Context context) {
+ return (!checkNetworkRequirements(context) ? getRequiredNetworkType() : 0)
+ | (!checkChargingRequirement(context) ? DEVICE_CHARGING : 0)
+ | (!checkIdleRequirement(context) ? DEVICE_IDLE : 0);
+ }
+
+ /** Returns the requirement flags. */
+ @RequirementFlags
+ public int getRequirements() {
return requirements;
}
@@ -239,4 +283,20 @@ public final class Requirements {
+ (isIdleRequired() ? ",idle" : "")
+ '}';
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ return requirements == ((Requirements) o).requirements;
+ }
+
+ @Override
+ public int hashCode() {
+ return requirements;
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java
index d1eb28cc2a..686f19d161 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java
@@ -44,18 +44,23 @@ public final class RequirementsWatcher {
public interface Listener {
/**
- * Called when the requirements are met.
+ * Called when all of the requirements are met.
*
* @param requirementsWatcher Calling instance.
*/
void requirementsMet(RequirementsWatcher requirementsWatcher);
/**
- * Called when the requirements are not met.
+ * Called when there is at least one not met requirement and there is a change on which of the
+ * requirements are not met.
*
* @param requirementsWatcher Calling instance.
+ * @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not
+ * met, or 0.
*/
- void requirementsNotMet(RequirementsWatcher requirementsWatcher);
+ void requirementsNotMet(
+ RequirementsWatcher requirementsWatcher,
+ @Requirements.RequirementFlags int notMetRequirements);
}
private static final String TAG = "RequirementsWatcher";
@@ -65,8 +70,9 @@ public final class RequirementsWatcher {
private final Requirements requirements;
private DeviceStatusChangeReceiver receiver;
- private boolean requirementsWereMet;
+ @Requirements.RequirementFlags private int notMetRequirements;
private CapabilityValidatedCallback networkCallback;
+ private Handler handler;
/**
* @param context Any context.
@@ -83,11 +89,15 @@ public final class RequirementsWatcher {
/**
* Starts watching for changes. Must be called from a thread that has an associated {@link
* Looper}. Listener methods are called on the caller thread.
+ *
+ * @return Initial {@link Requirements.RequirementFlags RequirementFlags} that are not met, or 0.
*/
- public void start() {
+ @Requirements.RequirementFlags
+ public int start() {
Assertions.checkNotNull(Looper.myLooper());
+ handler = new Handler();
- requirementsWereMet = requirements.checkRequirements(context);
+ notMetRequirements = requirements.getNotMetRequirements(context);
IntentFilter filter = new IntentFilter();
if (requirements.getRequiredNetworkType() != Requirements.NETWORK_TYPE_NONE) {
@@ -110,8 +120,9 @@ public final class RequirementsWatcher {
}
}
receiver = new DeviceStatusChangeReceiver();
- context.registerReceiver(receiver, filter, null, new Handler());
+ context.registerReceiver(receiver, filter, null, handler);
logd(this + " started");
+ return notMetRequirements;
}
/** Stops watching for changes. */
@@ -159,18 +170,19 @@ public final class RequirementsWatcher {
}
private void checkRequirements() {
- boolean requirementsAreMet = requirements.checkRequirements(context);
- if (requirementsAreMet == requirementsWereMet) {
- logd("requirementsAreMet is still " + requirementsAreMet);
+ @Requirements.RequirementFlags
+ int notMetRequirements = requirements.getNotMetRequirements(context);
+ if (this.notMetRequirements == notMetRequirements) {
+ logd("notMetRequirements hasn't changed: " + notMetRequirements);
return;
}
- requirementsWereMet = requirementsAreMet;
- if (requirementsAreMet) {
+ this.notMetRequirements = notMetRequirements;
+ if (notMetRequirements == 0) {
logd("start job");
listener.requirementsMet(this);
} else {
logd("stop job");
- listener.requirementsNotMet(this);
+ listener.requirementsNotMet(this, notMetRequirements);
}
}
@@ -194,16 +206,22 @@ public final class RequirementsWatcher {
private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback {
@Override
public void onAvailable(Network network) {
- super.onAvailable(network);
- logd(RequirementsWatcher.this + " NetworkCallback.onAvailable");
- checkRequirements();
+ onNetworkCallback();
}
@Override
public void onLost(Network network) {
- super.onLost(network);
- logd(RequirementsWatcher.this + " NetworkCallback.onLost");
- checkRequirements();
+ onNetworkCallback();
+ }
+
+ private void onNetworkCallback() {
+ handler.post(
+ () -> {
+ if (networkCallback != null) {
+ logd(RequirementsWatcher.this + " NetworkCallback");
+ checkRequirements();
+ }
+ });
}
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java
index 3d6e204c9c..189467b47e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java
@@ -16,8 +16,8 @@
package com.google.android.exoplayer2.source;
import android.os.Handler;
+import android.os.Looper;
import android.support.annotation.Nullable;
-import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
@@ -35,9 +35,9 @@ public abstract class BaseMediaSource implements MediaSource {
private final ArrayList sourceInfoListeners;
private final MediaSourceEventListener.EventDispatcher eventDispatcher;
- private @Nullable ExoPlayer player;
- private @Nullable Timeline timeline;
- private @Nullable Object manifest;
+ @Nullable private Looper looper;
+ @Nullable private Timeline timeline;
+ @Nullable private Object manifest;
public BaseMediaSource() {
sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1);
@@ -48,21 +48,16 @@ public abstract class BaseMediaSource implements MediaSource {
* Starts source preparation. This method is called at most once until the next call to {@link
* #releaseSourceInternal()}.
*
- * @param player The player for which this source is being prepared.
- * @param isTopLevelSource Whether this source has been passed directly to {@link
- * ExoPlayer#prepare(MediaSource)} or {@link ExoPlayer#prepare(MediaSource, boolean,
- * boolean)}.
* @param mediaTransferListener The transfer listener which should be informed of any media data
* transfers. May be null if no listener is available. Note that this listener should usually
* be only informed of transfers related to the media loads and not of auxiliary loads for
* manifests and other data.
*/
- protected abstract void prepareSourceInternal(
- ExoPlayer player, boolean isTopLevelSource, @Nullable TransferListener mediaTransferListener);
+ protected abstract void prepareSourceInternal(@Nullable TransferListener mediaTransferListener);
/**
* Releases the source. This method is called exactly once after each call to {@link
- * #prepareSourceInternal(ExoPlayer, boolean, TransferListener)}.
+ * #prepareSourceInternal(TransferListener)}.
*/
protected abstract void releaseSourceInternal();
@@ -135,15 +130,14 @@ public abstract class BaseMediaSource implements MediaSource {
@Override
public final void prepareSource(
- ExoPlayer player,
- boolean isTopLevelSource,
SourceInfoRefreshListener listener,
@Nullable TransferListener mediaTransferListener) {
- Assertions.checkArgument(this.player == null || this.player == player);
+ Looper looper = Looper.myLooper();
+ Assertions.checkArgument(this.looper == null || this.looper == looper);
sourceInfoListeners.add(listener);
- if (this.player == null) {
- this.player = player;
- prepareSourceInternal(player, isTopLevelSource, mediaTransferListener);
+ if (this.looper == null) {
+ this.looper = looper;
+ prepareSourceInternal(mediaTransferListener);
} else if (timeline != null) {
listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest);
}
@@ -153,7 +147,7 @@ public abstract class BaseMediaSource implements MediaSource {
public final void releaseSource(SourceInfoRefreshListener listener) {
sourceInfoListeners.remove(listener);
if (sourceInfoListeners.isEmpty()) {
- player = null;
+ looper = null;
timeline = null;
manifest = null;
releaseSourceInternal();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
index 1dbb41dfb0..d3b8226822 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
@@ -193,11 +192,8 @@ public final class ClippingMediaSource extends CompositeMediaSource {
}
@Override
- public void prepareSourceInternal(
- ExoPlayer player,
- boolean isTopLevelSource,
- @Nullable TransferListener mediaTransferListener) {
- super.prepareSourceInternal(player, isTopLevelSource, mediaTransferListener);
+ public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ super.prepareSourceInternal(mediaTransferListener);
prepareChildSource(/* id= */ null, mediaSource);
}
@@ -210,10 +206,10 @@ public final class ClippingMediaSource extends CompositeMediaSource {
}
@Override
- public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
ClippingMediaPeriod mediaPeriod =
new ClippingMediaPeriod(
- mediaSource.createPeriod(id, allocator),
+ mediaSource.createPeriod(id, allocator, startPositionUs),
enableInitialDiscontinuity,
periodStartUs,
periodEndUs);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java
index 69fa4b094b..dbf5812f98 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java
@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source;
import android.os.Handler;
import android.support.annotation.CallSuper;
import android.support.annotation.Nullable;
-import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
@@ -35,7 +34,6 @@ public abstract class CompositeMediaSource extends BaseMediaSource {
private final HashMap childSources;
- private @Nullable ExoPlayer player;
private @Nullable Handler eventHandler;
private @Nullable TransferListener mediaTransferListener;
@@ -46,11 +44,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource {
@Override
@CallSuper
- public void prepareSourceInternal(
- ExoPlayer player,
- boolean isTopLevelSource,
- @Nullable TransferListener mediaTransferListener) {
- this.player = player;
+ public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
this.mediaTransferListener = mediaTransferListener;
eventHandler = new Handler();
}
@@ -71,7 +65,6 @@ public abstract class CompositeMediaSource extends BaseMediaSource {
childSource.mediaSource.removeEventListener(childSource.eventListener);
}
childSources.clear();
- player = null;
}
/**
@@ -105,11 +98,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource {
MediaSourceEventListener eventListener = new ForwardingEventListener(id);
childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener));
mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener);
- mediaSource.prepareSource(
- Assertions.checkNotNull(player),
- /* isTopLevelSource= */ false,
- sourceListener,
- mediaTransferListener);
+ mediaSource.prepareSource(sourceListener, mediaTransferListener);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
index 26667e641f..6dc7a0a327 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
@@ -16,19 +16,19 @@
package com.google.android.exoplayer2.source;
import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.GuardedBy;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.PlayerMessage;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder;
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.EventDispatcher;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
@@ -45,8 +45,7 @@ import java.util.Map;
* during playback. It is valid for the same {@link MediaSource} instance to be present more than
* once in the concatenation. Access to this class is thread-safe.
*/
-public class ConcatenatingMediaSource extends CompositeMediaSource
- implements PlayerMessage.Target {
+public class ConcatenatingMediaSource extends CompositeMediaSource {
private static final int MSG_ADD = 0;
private static final int MSG_REMOVE = 1;
@@ -55,22 +54,21 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourcesPublic;
+ @Nullable private Handler playbackThreadHandler;
- // Accessed on the playback thread.
+ // Accessed on the playback thread only.
private final List mediaSourceHolders;
private final Map mediaSourceByMediaPeriod;
private final Map