diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
index 690069ffa8..c0980df440 100644
--- a/.github/ISSUE_TEMPLATE/bug.md
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -8,9 +8,12 @@ assignees: ''
Before filing a bug:
-----------------------
-- Search existing issues, including issues that are closed.
-- Consult our FAQs, supported devices and supported formats pages. These can be
- found at https://exoplayer.dev/.
+- Search existing issues, including issues that are closed:
+ https://github.com/google/ExoPlayer/issues?q=is%3Aissue
+- Consult our developer website, which can be found at https://exoplayer.dev/.
+ It provides detailed information about supported formats and devices.
+- Learn how to create useful log output by using the EventLogger:
+ https://exoplayer.dev/listening-to-player-events.html#using-eventlogger
- Rule out issues in your own code. A good way to do this is to try and
reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
demo app can be found here:
@@ -33,16 +36,17 @@ or a small sample app that you’re able to share as source code on GitHub.
Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to
media that reproduces the issue. If you don't wish to post it publicly, please
submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
-in the format "Issue #1234". Provide all the metadata we'd need to play the
-content like drm license urls or similar. If the content is accessible only in
-certain countries or regions, please say so.
+in the format "Issue #1234", where "#1234" should be replaced with your issue
+number. Provide all the metadata we'd need to play the content like drm license
+urls or similar. If the content is accessible only in certain countries or
+regions, please say so.
### [REQUIRED] A full bug report captured from the device
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
log snippet is NOT sufficient. Please attach the captured bug report as a file.
If you don't wish to post it publicly, please submit the issue, then email the
bug report to dev.exoplayer@gmail.com using a subject in the format
-"Issue #1234".
+"Issue #1234", where "#1234" should be replaced with your issue number.
### [REQUIRED] Version of ExoPlayer being used
Specify the absolute version number. Avoid using terms such as "latest".
diff --git a/.github/ISSUE_TEMPLATE/content_not_playing.md b/.github/ISSUE_TEMPLATE/content_not_playing.md
index f326e7cd46..c8d4668a6a 100644
--- a/.github/ISSUE_TEMPLATE/content_not_playing.md
+++ b/.github/ISSUE_TEMPLATE/content_not_playing.md
@@ -8,9 +8,12 @@ assignees: ''
Before filing a content issue:
------------------------------
-- Search existing issues, including issues that are closed.
+- Search existing issues, including issues that are closed:
+ https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- Consult our supported formats page, which can be found at
https://exoplayer.dev/supported-formats.html.
+- Learn how to create useful log output by using the EventLogger:
+ https://exoplayer.dev/listening-to-player-events.html#using-eventlogger
- Try playing your content in the ExoPlayer demo app. Information about the
ExoPlayer demo app can be found here:
http://exoplayer.dev/demo-application.html.
@@ -30,9 +33,10 @@ and you expect to play, like 5.1 audio track, text tracks or drm systems.
Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to
media that reproduces the issue. If you don't wish to post it publicly, please
submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
-in the format "Issue #1234". Provide all the metadata we'd need to play the
-content like drm license urls or similar. If the content is accessible only in
-certain countries or regions, please say so.
+in the format "Issue #1234", where "#1234" should be replaced with your issue
+number. Provide all the metadata we'd need to play the content like drm license
+urls or similar. If the content is accessible only in certain countries or
+regions, please say so.
### [REQUIRED] Version of ExoPlayer being used
Specify the absolute version number. Avoid using terms such as "latest".
@@ -41,6 +45,13 @@ Specify the absolute version number. Avoid using terms such as "latest".
Specify the devices and versions of Android on which you expect the content to
play. If possible, please test on multiple devices and Android versions.
+### [REQUIRED] A full bug report captured from the device
+Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
+log snippet is NOT sufficient. Please attach the captured bug report as a file.
+If you don't wish to post it publicly, please submit the issue, then email the
+bug report to dev.exoplayer@gmail.com using a subject in the format
+"Issue #1234", where "#1234" should be replaced with your issue number.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java
new file mode 100644
index 0000000000..059f26b374
--- /dev/null
+++ b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/PlayerActivity.java
@@ -0,0 +1,242 @@
+/*
+ * 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.gvrdemo;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+import android.widget.Toast;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.C.ContentType;
+import com.google.android.exoplayer2.DefaultRenderersFactory;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayerFactory;
+import com.google.android.exoplayer2.PlaybackPreparer;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.ext.gvr.GvrPlayerActivity;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.dash.DashMediaSource;
+import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.exoplayer2.util.EventLogger;
+import com.google.android.exoplayer2.util.Util;
+
+/** An activity that plays media using {@link SimpleExoPlayer}. */
+public class PlayerActivity extends GvrPlayerActivity implements PlaybackPreparer {
+
+ public static final String EXTENSION_EXTRA = "extension";
+
+ public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
+ public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
+ public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
+ public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
+
+ private DataSource.Factory dataSourceFactory;
+ private SimpleExoPlayer player;
+ private MediaSource mediaSource;
+ private DefaultTrackSelector trackSelector;
+ private TrackGroupArray lastSeenTrackGroupArray;
+
+ private boolean startAutoPlay;
+ private int startWindow;
+ private long startPosition;
+
+ // Activity lifecycle
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ String userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
+ dataSourceFactory =
+ new DefaultDataSourceFactory(this, new DefaultHttpDataSourceFactory(userAgent));
+
+ String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
+ if (sphericalStereoMode != null) {
+ int stereoMode;
+ if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
+ stereoMode = C.STEREO_MODE_MONO;
+ } else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
+ stereoMode = C.STEREO_MODE_TOP_BOTTOM;
+ } else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
+ stereoMode = C.STEREO_MODE_LEFT_RIGHT;
+ } else {
+ showToast(R.string.error_unrecognized_stereo_mode);
+ finish();
+ return;
+ }
+ setDefaultStereoMode(stereoMode);
+ }
+
+ clearStartPosition();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (Util.SDK_INT <= 23 || player == null) {
+ initializePlayer();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (Util.SDK_INT <= 23) {
+ releasePlayer();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ // PlaybackControlView.PlaybackPreparer implementation
+
+ @Override
+ public void preparePlayback() {
+ initializePlayer();
+ }
+
+ // Internal methods
+
+ private void initializePlayer() {
+ if (player == null) {
+ Intent intent = getIntent();
+ Uri uri = intent.getData();
+ if (!Util.checkCleartextTrafficPermitted(uri)) {
+ showToast(R.string.error_cleartext_not_permitted);
+ return;
+ }
+
+ DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this);
+
+ trackSelector = new DefaultTrackSelector(/* context= */ this);
+ lastSeenTrackGroupArray = null;
+
+ player =
+ ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector);
+ player.addListener(new PlayerEventListener());
+ player.setPlayWhenReady(startAutoPlay);
+ player.addAnalyticsListener(new EventLogger(trackSelector));
+ setPlayer(player);
+
+ mediaSource = buildMediaSource(uri, intent.getStringExtra(EXTENSION_EXTRA));
+ }
+ boolean haveStartPosition = startWindow != C.INDEX_UNSET;
+ if (haveStartPosition) {
+ player.seekTo(startWindow, startPosition);
+ }
+ player.prepare(mediaSource, !haveStartPosition, false);
+ }
+
+ private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
+ @ContentType int type = Util.inferContentType(uri, overrideExtension);
+ switch (type) {
+ case C.TYPE_DASH:
+ return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
+ case C.TYPE_SS:
+ return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
+ case C.TYPE_HLS:
+ return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
+ case C.TYPE_OTHER:
+ return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
+ default:
+ throw new IllegalStateException("Unsupported type: " + type);
+ }
+ }
+
+ private void releasePlayer() {
+ if (player != null) {
+ updateStartPosition();
+ player.release();
+ player = null;
+ mediaSource = null;
+ trackSelector = null;
+ }
+ }
+
+ private void updateStartPosition() {
+ if (player != null) {
+ startAutoPlay = player.getPlayWhenReady();
+ startWindow = player.getCurrentWindowIndex();
+ startPosition = Math.max(0, player.getContentPosition());
+ }
+ }
+
+ private void clearStartPosition() {
+ startAutoPlay = true;
+ startWindow = C.INDEX_UNSET;
+ startPosition = C.TIME_UNSET;
+ }
+
+ private void showToast(int messageId) {
+ showToast(getString(messageId));
+ }
+
+ private void showToast(String message) {
+ Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
+ }
+
+ private class PlayerEventListener implements Player.EventListener {
+
+ @Override
+ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {}
+
+ @Override
+ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ if (player.getPlaybackError() != null) {
+ // The user has performed a seek whilst in the error state. Update the resume position so
+ // that if the user then retries, playback resumes from the position to which they seeked.
+ updateStartPosition();
+ }
+ }
+
+ @Override
+ public void onPlayerError(ExoPlaybackException e) {
+ updateStartPosition();
+ }
+
+ @Override
+ @SuppressWarnings("ReferenceEquality")
+ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ if (trackGroups != lastSeenTrackGroupArray) {
+ MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
+ if (mappedTrackInfo != null) {
+ if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
+ == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
+ showToast(R.string.error_unsupported_video);
+ }
+ if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
+ == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
+ showToast(R.string.error_unsupported_audio);
+ }
+ }
+ lastSeenTrackGroupArray = trackGroups;
+ }
+ }
+ }
+}
diff --git a/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/SampleChooserActivity.java b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/SampleChooserActivity.java
new file mode 100644
index 0000000000..1ddf5c1517
--- /dev/null
+++ b/demos/gvr/src/main/java/com/google/android/exoplayer2/gvrdemo/SampleChooserActivity.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 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.gvrdemo;
+
+import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_LEFT_RIGHT;
+import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_MONO;
+import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_TOP_BOTTOM;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+/** An activity for selecting from a list of media samples. */
+public class SampleChooserActivity extends Activity {
+
+ private final Sample[] samples =
+ new Sample[] {
+ new Sample(
+ "Congo (360 top-bottom stereo)",
+ "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
+ SPHERICAL_STEREO_MODE_TOP_BOTTOM),
+ new Sample(
+ "Sphericalv2 (180 top-bottom stereo)",
+ "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4",
+ SPHERICAL_STEREO_MODE_TOP_BOTTOM),
+ new Sample(
+ "Iceland (360 top-bottom stereo ts)",
+ "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
+ SPHERICAL_STEREO_MODE_TOP_BOTTOM),
+ new Sample(
+ "Camera motion metadata test",
+ "https://storage.googleapis.com/exoplayer-test-media-internal-"
+ + "63834241aced7884c2544af1a3452e01/vr180/synthetic_with_camm.mp4",
+ SPHERICAL_STEREO_MODE_TOP_BOTTOM),
+ new Sample(
+ "actual_camera_cat",
+ "https://storage.googleapis.com/exoplayer-test-media-internal-"
+ + "63834241aced7884c2544af1a3452e01/vr180/actual_camera_cat.mp4",
+ SPHERICAL_STEREO_MODE_TOP_BOTTOM),
+ new Sample(
+ "johnny_stitched",
+ "https://storage.googleapis.com/exoplayer-test-media-internal-"
+ + "63834241aced7884c2544af1a3452e01/vr180/johnny_stitched.mp4",
+ SPHERICAL_STEREO_MODE_TOP_BOTTOM),
+ new Sample(
+ "lenovo_birds.vr",
+ "https://storage.googleapis.com/exoplayer-test-media-internal-"
+ + "63834241aced7884c2544af1a3452e01/vr180/lenovo_birds.vr.mp4",
+ SPHERICAL_STEREO_MODE_TOP_BOTTOM),
+ new Sample(
+ "mono_v1_sample",
+ "https://storage.googleapis.com/exoplayer-test-media-internal-"
+ + "63834241aced7884c2544af1a3452e01/vr180/mono_v1_sample.mp4",
+ SPHERICAL_STEREO_MODE_MONO),
+ new Sample(
+ "not_vr180_actually_shot_with_moto_mod",
+ "https://storage.googleapis.com/exoplayer-test-media-internal-"
+ + "63834241aced7884c2544af1a3452e01/vr180/"
+ + "not_vr180_actually_shot_with_moto_mod.mp4",
+ SPHERICAL_STEREO_MODE_TOP_BOTTOM),
+ new Sample(
+ "stereo_v1_sample",
+ "https://storage.googleapis.com/exoplayer-test-media-internal-"
+ + "63834241aced7884c2544af1a3452e01/vr180/stereo_v1_sample.mp4",
+ SPHERICAL_STEREO_MODE_TOP_BOTTOM),
+ new Sample(
+ "yi_giraffes.vr",
+ "https://storage.googleapis.com/exoplayer-test-media-internal-"
+ + "63834241aced7884c2544af1a3452e01/vr180/yi_giraffes.vr.mp4",
+ SPHERICAL_STEREO_MODE_TOP_BOTTOM),
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.sample_chooser_activity);
+ ListView sampleListView = findViewById(R.id.sample_list);
+ sampleListView.setAdapter(
+ new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, samples));
+ sampleListView.setOnItemClickListener(
+ (parent, view, position, id) ->
+ startActivity(
+ samples[position].buildIntent(/* context= */ SampleChooserActivity.this)));
+ }
+
+ private static final class Sample {
+ public final String name;
+ public final String uri;
+ public final String extension;
+ public final String sphericalStereoMode;
+
+ public Sample(String name, String uri, String sphericalStereoMode) {
+ this(name, uri, sphericalStereoMode, null);
+ }
+
+ public Sample(String name, String uri, String sphericalStereoMode, String extension) {
+ this.name = name;
+ this.uri = uri;
+ this.extension = extension;
+ this.sphericalStereoMode = sphericalStereoMode;
+ }
+
+ public Intent buildIntent(Context context) {
+ Intent intent = new Intent(context, PlayerActivity.class);
+ return intent
+ .setData(Uri.parse(uri))
+ .putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
+ .putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+}
diff --git a/testutils_robolectric/src/main/AndroidManifest.xml b/demos/gvr/src/main/res/layout/sample_chooser_activity.xml
similarity index 57%
rename from testutils_robolectric/src/main/AndroidManifest.xml
rename to demos/gvr/src/main/res/layout/sample_chooser_activity.xml
index 057caad867..ce520e70e4 100644
--- a/testutils_robolectric/src/main/AndroidManifest.xml
+++ b/demos/gvr/src/main/res/layout/sample_chooser_activity.xml
@@ -1,5 +1,5 @@
-
-
+
-
+
+
+
diff --git a/demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..adaa93220e
Binary files /dev/null and b/demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/gvr/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..9b6f7d5e80
Binary files /dev/null and b/demos/gvr/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..2101026c9f
Binary files /dev/null and b/demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..223ec8bd11
Binary files /dev/null and b/demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/gvr/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/gvr/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..698ed68c42
Binary files /dev/null and b/demos/gvr/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/demos/gvr/src/main/res/values/strings.xml b/demos/gvr/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..08feccb398
--- /dev/null
+++ b/demos/gvr/src/main/res/values/strings.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ ExoPlayer VR Demo
+
+ Cleartext traffic not permitted
+
+ Unrecognized stereo mode
+
+ Media includes video tracks, but none are playable by this device
+
+ Media includes audio tracks, but none are playable by this device
+
+
diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle
index 33161b4121..124555d9b5 100644
--- a/demos/ima/build.gradle
+++ b/demos/ima/build.gradle
@@ -53,7 +53,7 @@ dependencies {
implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'extension-ima')
- implementation 'androidx.annotation:annotation:1.0.2'
+ implementation 'androidx.annotation:annotation:1.1.0'
}
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
diff --git a/demos/main/build.gradle b/demos/main/build.gradle
index 0bce1d4b82..f58389d9d4 100644
--- a/demos/main/build.gradle
+++ b/demos/main/build.gradle
@@ -62,7 +62,7 @@ android {
}
dependencies {
- implementation 'androidx.annotation:annotation:1.0.2'
+ implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.viewpager:viewpager:1.0.0'
implementation 'androidx.fragment:fragment:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java
index 3886ef5c44..c3909dfe46 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java
@@ -41,7 +41,8 @@ public class DemoDownloadService extends DownloadService {
FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
CHANNEL_ID,
- R.string.exo_download_notification_channel_name);
+ R.string.exo_download_notification_channel_name,
+ /* channelDescriptionResourceId= */ 0);
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
index e1e866bbee..839ed304bd 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
@@ -30,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadIndex;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloadService;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Log;
@@ -55,6 +56,7 @@ public class DownloadTracker {
private final CopyOnWriteArraySet listeners;
private final HashMap downloads;
private final DownloadIndex downloadIndex;
+ private final DefaultTrackSelector.Parameters trackSelectorParameters;
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
@@ -65,6 +67,7 @@ public class DownloadTracker {
listeners = new CopyOnWriteArraySet<>();
downloads = new HashMap<>();
downloadIndex = downloadManager.getDownloadIndex();
+ trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context);
downloadManager.addListener(new DownloadManagerListener());
loadDownloads();
}
@@ -123,13 +126,13 @@ public class DownloadTracker {
int type = Util.inferContentType(uri, extension);
switch (type) {
case C.TYPE_DASH:
- return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory);
+ return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_SS:
- return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory);
+ return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_HLS:
- return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory);
+ return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_OTHER:
- return DownloadHelper.forProgressive(uri);
+ return DownloadHelper.forProgressive(context, uri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
@@ -202,7 +205,7 @@ public class DownloadTracker {
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
/* titleId= */ R.string.exo_download_description,
mappedTrackInfo,
- /* initialParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
+ trackSelectorParameters,
/* allowAdaptiveSelections =*/ false,
/* allowMultipleOverrides= */ true,
/* onClickListener= */ this,
@@ -212,9 +215,7 @@ public class DownloadTracker {
@Override
public void onPrepareError(DownloadHelper helper, IOException e) {
- Toast.makeText(
- context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
- .show();
+ Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
Log.e(TAG, "Failed to start download", e);
}
@@ -229,7 +230,7 @@ public class DownloadTracker {
downloadHelper.addTrackSelectionForSingleRenderer(
periodIndex,
/* rendererIndex= */ i,
- DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
+ trackSelectorParameters,
trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
}
}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
index 8ee9e9f9f6..1e231dd45e 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
@@ -38,7 +38,9 @@ import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.demo.Sample.UriSample;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
@@ -77,41 +79,48 @@ import java.lang.reflect.Constructor;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
+import java.util.ArrayList;
import java.util.UUID;
/** An activity that plays media using {@link SimpleExoPlayer}. */
public class PlayerActivity extends AppCompatActivity
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
- public static final String DRM_SCHEME_EXTRA = "drm_scheme";
- public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
- public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
- public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
- public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
-
- public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
- public static final String EXTENSION_EXTRA = "extension";
-
- public static final String ACTION_VIEW_LIST =
- "com.google.android.exoplayer.demo.action.VIEW_LIST";
- public static final String URI_LIST_EXTRA = "uri_list";
- public static final String EXTENSION_LIST_EXTRA = "extension_list";
-
- public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
-
- public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
- public static final String ABR_ALGORITHM_DEFAULT = "default";
- public static final String ABR_ALGORITHM_RANDOM = "random";
+ // Activity extras.
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
+ // Actions.
+
+ public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
+ public static final String ACTION_VIEW_LIST =
+ "com.google.android.exoplayer.demo.action.VIEW_LIST";
+
+ // Player configuration extras.
+
+ public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
+ public static final String ABR_ALGORITHM_DEFAULT = "default";
+ public static final String ABR_ALGORITHM_RANDOM = "random";
+
+ // Media item configuration extras.
+
+ public static final String URI_EXTRA = "uri";
+ public static final String EXTENSION_EXTRA = "extension";
+
+ public static final String DRM_SCHEME_EXTRA = "drm_scheme";
+ public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
+ public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
+ public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
+ public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
+ public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
// For backwards compatibility only.
- private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
+ public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
// Saved instance state keys.
+
private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
private static final String KEY_WINDOW = "window";
private static final String KEY_POSITION = "position";
@@ -123,6 +132,8 @@ public class PlayerActivity extends AppCompatActivity
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}
+ private final ArrayList mediaDrms;
+
private PlayerView playerView;
private LinearLayout debugRootView;
private Button selectTracksButton;
@@ -131,7 +142,6 @@ public class PlayerActivity extends AppCompatActivity
private DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player;
- private FrameworkMediaDrm mediaDrm;
private MediaSource mediaSource;
private DefaultTrackSelector trackSelector;
private DefaultTrackSelector.Parameters trackSelectorParameters;
@@ -147,11 +157,16 @@ public class PlayerActivity extends AppCompatActivity
private AdsLoader adsLoader;
private Uri loadedAdTagUri;
+ public PlayerActivity() {
+ mediaDrms = new ArrayList<>();
+ }
+
// Activity lifecycle
@Override
public void onCreate(Bundle savedInstanceState) {
- String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
+ Intent intent = getIntent();
+ String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
if (sphericalStereoMode != null) {
setTheme(R.style.PlayerTheme_Spherical);
}
@@ -193,7 +208,7 @@ public class PlayerActivity extends AppCompatActivity
startWindow = savedInstanceState.getInt(KEY_WINDOW);
startPosition = savedInstanceState.getLong(KEY_POSITION);
} else {
- trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build();
+ trackSelectorParameters = DefaultTrackSelector.Parameters.getDefaults(/* context= */ this);
clearStartPosition();
}
}
@@ -327,67 +342,11 @@ public class PlayerActivity extends AppCompatActivity
private void initializePlayer() {
if (player == null) {
Intent intent = getIntent();
- String action = intent.getAction();
- Uri[] uris;
- String[] extensions;
- if (ACTION_VIEW.equals(action)) {
- uris = new Uri[] {intent.getData()};
- extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)};
- } else if (ACTION_VIEW_LIST.equals(action)) {
- String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
- uris = new Uri[uriStrings.length];
- for (int i = 0; i < uriStrings.length; i++) {
- uris[i] = Uri.parse(uriStrings[i]);
- }
- extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);
- if (extensions == null) {
- extensions = new String[uriStrings.length];
- }
- } else {
- showToast(getString(R.string.unexpected_intent_action, action));
- finish();
- return;
- }
- if (!Util.checkCleartextTrafficPermitted(uris)) {
- showToast(R.string.error_cleartext_not_permitted);
- return;
- }
- if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) {
- // The player will be reinitialized if the permission is granted.
- return;
- }
- DefaultDrmSessionManager drmSessionManager = null;
- if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) {
- String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA);
- String[] keyRequestPropertiesArray =
- intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA);
- boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false);
- int errorStringId = R.string.error_drm_unknown;
- if (Util.SDK_INT < 18) {
- errorStringId = R.string.error_drm_not_supported;
- } else {
- try {
- String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA
- : DRM_SCHEME_UUID_EXTRA;
- UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra));
- if (drmSchemeUuid == null) {
- errorStringId = R.string.error_drm_unsupported_scheme;
- } else {
- drmSessionManager =
- buildDrmSessionManagerV18(
- drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession);
- }
- } catch (UnsupportedDrmException e) {
- errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
- ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown;
- }
- }
- if (drmSessionManager == null) {
- showToast(errorStringId);
- finish();
- return;
- }
+ releaseMediaDrms();
+ mediaSource = createTopLevelMediaSource(intent);
+ if (mediaSource == null) {
+ return;
}
TrackSelection.Factory trackSelectionFactory;
@@ -407,13 +366,12 @@ public class PlayerActivity extends AppCompatActivity
RenderersFactory renderersFactory =
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
- trackSelector = new DefaultTrackSelector(trackSelectionFactory);
+ trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory);
trackSelector.setParameters(trackSelectorParameters);
lastSeenTrackGroupArray = null;
player =
- ExoPlayerFactory.newSimpleInstance(
- /* context= */ this, renderersFactory, trackSelector, drmSessionManager);
+ ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector);
player.addListener(new PlayerEventListener());
player.setPlayWhenReady(startAutoPlay);
player.addAnalyticsListener(new EventLogger(trackSelector));
@@ -421,28 +379,8 @@ public class PlayerActivity extends AppCompatActivity
playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
-
- MediaSource[] mediaSources = new MediaSource[uris.length];
- for (int i = 0; i < uris.length; i++) {
- mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
- }
- mediaSource =
- mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
- String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
- if (adTagUriString != null) {
- Uri adTagUri = Uri.parse(adTagUriString);
- if (!adTagUri.equals(loadedAdTagUri)) {
- releaseAdsLoader();
- loadedAdTagUri = adTagUri;
- }
- MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
- if (adsMediaSource != null) {
- mediaSource = adsMediaSource;
- } else {
- showToast(R.string.ima_not_loaded);
- }
- } else {
- releaseAdsLoader();
+ if (adsLoader != null) {
+ adsLoader.setPlayer(player);
}
}
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
@@ -453,26 +391,130 @@ public class PlayerActivity extends AppCompatActivity
updateButtonVisibility();
}
- private MediaSource buildMediaSource(Uri uri) {
- return buildMediaSource(uri, null);
+ @Nullable
+ private MediaSource createTopLevelMediaSource(Intent intent) {
+ String action = intent.getAction();
+ boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
+ if (!actionIsListView && !ACTION_VIEW.equals(action)) {
+ showToast(getString(R.string.unexpected_intent_action, action));
+ finish();
+ return null;
+ }
+
+ Sample intentAsSample = Sample.createFromIntent(intent);
+ UriSample[] samples =
+ intentAsSample instanceof Sample.PlaylistSample
+ ? ((Sample.PlaylistSample) intentAsSample).children
+ : new UriSample[] {(UriSample) intentAsSample};
+
+ boolean seenAdsTagUri = false;
+ for (UriSample sample : samples) {
+ seenAdsTagUri |= sample.adTagUri != null;
+ if (!Util.checkCleartextTrafficPermitted(sample.uri)) {
+ showToast(R.string.error_cleartext_not_permitted);
+ return null;
+ }
+ if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) {
+ // The player will be reinitialized if the permission is granted.
+ return null;
+ }
+ }
+
+ MediaSource[] mediaSources = new MediaSource[samples.length];
+ for (int i = 0; i < samples.length; i++) {
+ mediaSources[i] = createLeafMediaSource(samples[i]);
+ }
+ MediaSource mediaSource =
+ mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
+
+ if (seenAdsTagUri) {
+ Uri adTagUri = samples[0].adTagUri;
+ if (actionIsListView) {
+ showToast(R.string.unsupported_ads_in_concatenation);
+ } else {
+ if (!adTagUri.equals(loadedAdTagUri)) {
+ releaseAdsLoader();
+ loadedAdTagUri = adTagUri;
+ }
+ MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri);
+ if (adsMediaSource != null) {
+ mediaSource = adsMediaSource;
+ } else {
+ showToast(R.string.ima_not_loaded);
+ }
+ }
+ } else {
+ releaseAdsLoader();
+ }
+
+ return mediaSource;
}
- private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
+ private MediaSource createLeafMediaSource(UriSample parameters) {
+ DrmSessionManager drmSessionManager = null;
+ Sample.DrmInfo drmInfo = parameters.drmInfo;
+ if (drmInfo != null) {
+ int errorStringId = R.string.error_drm_unknown;
+ if (Util.SDK_INT < 18) {
+ errorStringId = R.string.error_drm_not_supported;
+ } else {
+ try {
+ if (drmInfo.drmScheme == null) {
+ errorStringId = R.string.error_drm_unsupported_scheme;
+ } else {
+ drmSessionManager =
+ buildDrmSessionManagerV18(
+ drmInfo.drmScheme,
+ drmInfo.drmLicenseUrl,
+ drmInfo.drmKeyRequestProperties,
+ drmInfo.drmMultiSession);
+ }
+ } catch (UnsupportedDrmException e) {
+ errorStringId =
+ e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
+ ? R.string.error_drm_unsupported_scheme
+ : R.string.error_drm_unknown;
+ }
+ }
+ if (drmSessionManager == null) {
+ showToast(errorStringId);
+ finish();
+ return null;
+ }
+ } else {
+ drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
+ }
+
DownloadRequest downloadRequest =
- ((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri);
+ ((DemoApplication) getApplication())
+ .getDownloadTracker()
+ .getDownloadRequest(parameters.uri);
if (downloadRequest != null) {
return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
}
- @ContentType int type = Util.inferContentType(uri, overrideExtension);
+ return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager);
+ }
+
+ private MediaSource createLeafMediaSource(
+ Uri uri, String extension, DrmSessionManager drmSessionManager) {
+ @ContentType int type = Util.inferContentType(uri, extension);
switch (type) {
case C.TYPE_DASH:
- return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
+ return new DashMediaSource.Factory(dataSourceFactory)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
case C.TYPE_SS:
- return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
+ return new SsMediaSource.Factory(dataSourceFactory)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
case C.TYPE_HLS:
- return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
+ return new HlsMediaSource.Factory(dataSourceFactory)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
case C.TYPE_OTHER:
- return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
+ return new ProgressiveMediaSource.Factory(dataSourceFactory)
+ .setDrmSessionManager(drmSessionManager)
+ .createMediaSource(uri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
@@ -491,8 +533,9 @@ public class PlayerActivity extends AppCompatActivity
keyRequestPropertiesArray[i + 1]);
}
}
- releaseMediaDrm();
- mediaDrm = FrameworkMediaDrm.newInstance(uuid);
+
+ FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid);
+ mediaDrms.add(mediaDrm);
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
}
@@ -510,14 +553,14 @@ public class PlayerActivity extends AppCompatActivity
if (adsLoader != null) {
adsLoader.setPlayer(null);
}
- releaseMediaDrm();
+ releaseMediaDrms();
}
- private void releaseMediaDrm() {
- if (mediaDrm != null) {
+ private void releaseMediaDrms() {
+ for (FrameworkMediaDrm mediaDrm : mediaDrms) {
mediaDrm.release();
- mediaDrm = null;
}
+ mediaDrms.clear();
}
private void releaseAdsLoader() {
@@ -555,7 +598,8 @@ public class PlayerActivity extends AppCompatActivity
}
/** Returns an ads media source, reusing the ads loader if one exists. */
- private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
+ @Nullable
+ private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
// Load the extension source using reflection so the demo app doesn't have to depend on it.
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
try {
@@ -570,12 +614,12 @@ public class PlayerActivity extends AppCompatActivity
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
adsLoader = loaderConstructor.newInstance(this, adTagUri);
}
- adsLoader.setPlayer(player);
MediaSourceFactory adMediaSourceFactory =
new MediaSourceFactory() {
@Override
public MediaSource createMediaSource(Uri uri) {
- return PlayerActivity.this.buildMediaSource(uri);
+ return PlayerActivity.this.createLeafMediaSource(
+ uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager());
}
@Override
@@ -678,7 +722,7 @@ public class PlayerActivity extends AppCompatActivity
// Special case for decoder initialization failures.
DecoderInitializationException decoderInitializationException =
(DecoderInitializationException) cause;
- if (decoderInitializationException.decoderName == null) {
+ if (decoderInitializationException.codecInfo == null) {
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
errorString = getString(R.string.error_querying_decoders);
} else if (decoderInitializationException.secureDecoderRequired) {
@@ -693,12 +737,11 @@ public class PlayerActivity extends AppCompatActivity
errorString =
getString(
R.string.error_instantiating_decoder,
- decoderInitializationException.decoderName);
+ decoderInitializationException.codecInfo.name);
}
}
}
return Pair.create(0, errorString);
}
}
-
}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java
new file mode 100644
index 0000000000..4497b9a984
--- /dev/null
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java
@@ -0,0 +1,187 @@
+/*
+ * 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.demo;
+
+import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST;
+import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA;
+import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA;
+
+import android.content.Intent;
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.UUID;
+
+/* package */ abstract class Sample {
+
+ public static final class UriSample extends Sample {
+
+ public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) {
+ String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
+ String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix);
+ Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null;
+ return new UriSample(
+ /* name= */ null,
+ DrmInfo.createFromIntent(intent, extrasKeySuffix),
+ uri,
+ extension,
+ adTagUri,
+ /* sphericalStereoMode= */ null);
+ }
+
+ public final Uri uri;
+ public final String extension;
+ public final DrmInfo drmInfo;
+ public final Uri adTagUri;
+ public final String sphericalStereoMode;
+
+ public UriSample(
+ String name,
+ DrmInfo drmInfo,
+ Uri uri,
+ String extension,
+ Uri adTagUri,
+ String sphericalStereoMode) {
+ super(name);
+ this.uri = uri;
+ this.extension = extension;
+ this.drmInfo = drmInfo;
+ this.adTagUri = adTagUri;
+ this.sphericalStereoMode = sphericalStereoMode;
+ }
+
+ @Override
+ public void addToIntent(Intent intent) {
+ intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri);
+ intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
+ addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ "");
+ }
+
+ public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) {
+ intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString());
+ addPlayerConfigToIntent(intent, extrasKeySuffix);
+ }
+
+ private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) {
+ intent
+ .putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension)
+ .putExtra(
+ AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null);
+ if (drmInfo != null) {
+ drmInfo.addToIntent(intent, extrasKeySuffix);
+ }
+ }
+ }
+
+ public static final class PlaylistSample extends Sample {
+
+ public final UriSample[] children;
+
+ public PlaylistSample(String name, UriSample... children) {
+ super(name);
+ this.children = children;
+ }
+
+ @Override
+ public void addToIntent(Intent intent) {
+ intent.setAction(PlayerActivity.ACTION_VIEW_LIST);
+ for (int i = 0; i < children.length; i++) {
+ children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i);
+ }
+ }
+ }
+
+ public static final class DrmInfo {
+
+ public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) {
+ String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
+ String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
+ if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
+ return null;
+ }
+ String drmSchemeExtra =
+ intent.hasExtra(schemeKey)
+ ? intent.getStringExtra(schemeKey)
+ : intent.getStringExtra(schemeUuidKey);
+ UUID drmScheme = Util.getDrmUuid(drmSchemeExtra);
+ String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix);
+ String[] keyRequestPropertiesArray =
+ intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
+ boolean drmMultiSession =
+ intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false);
+ return new DrmInfo(drmScheme, drmLicenseUrl, keyRequestPropertiesArray, drmMultiSession);
+ }
+
+ public final UUID drmScheme;
+ public final String drmLicenseUrl;
+ public final String[] drmKeyRequestProperties;
+ public final boolean drmMultiSession;
+
+ public DrmInfo(
+ UUID drmScheme,
+ String drmLicenseUrl,
+ String[] drmKeyRequestProperties,
+ boolean drmMultiSession) {
+ this.drmScheme = drmScheme;
+ this.drmLicenseUrl = drmLicenseUrl;
+ this.drmKeyRequestProperties = drmKeyRequestProperties;
+ this.drmMultiSession = drmMultiSession;
+ }
+
+ public void addToIntent(Intent intent, String extrasKeySuffix) {
+ Assertions.checkNotNull(intent);
+ intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString());
+ intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl);
+ intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
+ intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession);
+ }
+ }
+
+ public static Sample createFromIntent(Intent intent) {
+ if (ACTION_VIEW_LIST.equals(intent.getAction())) {
+ ArrayList intentUris = new ArrayList<>();
+ int index = 0;
+ while (intent.hasExtra(URI_EXTRA + "_" + index)) {
+ intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index));
+ index++;
+ }
+ UriSample[] children = new UriSample[intentUris.size()];
+ for (int i = 0; i < children.length; i++) {
+ Uri uri = Uri.parse(intentUris.get(i));
+ children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i);
+ }
+ return new PlaylistSample(/* name= */ null, children);
+ } else {
+ return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ "");
+ }
+ }
+
+ @Nullable public final String name;
+
+ public Sample(String name) {
+ this.name = name;
+ }
+
+ public abstract void addToIntent(Intent intent);
+}
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 7245de01c6..09fa62e51a 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
@@ -38,6 +38,9 @@ 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.demo.Sample.DrmInfo;
+import com.google.android.exoplayer2.demo.Sample.PlaylistSample;
+import com.google.android.exoplayer2.demo.Sample.UriSample;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
@@ -161,13 +164,17 @@ public class SampleChooserActivity extends AppCompatActivity
public boolean onChildClick(
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
Sample sample = (Sample) view.getTag();
- startActivity(
- sample.buildIntent(
- /* context= */ this,
- isNonNullAndChecked(preferExtensionDecodersMenuItem),
- isNonNullAndChecked(randomAbrMenuItem)
- ? PlayerActivity.ABR_ALGORITHM_RANDOM
- : PlayerActivity.ABR_ALGORITHM_DEFAULT));
+ Intent intent = new Intent(this, PlayerActivity.class);
+ intent.putExtra(
+ PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA,
+ isNonNullAndChecked(preferExtensionDecodersMenuItem));
+ String abrAlgorithm =
+ isNonNullAndChecked(randomAbrMenuItem)
+ ? PlayerActivity.ABR_ALGORITHM_RANDOM
+ : PlayerActivity.ABR_ALGORITHM_DEFAULT;
+ intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
+ sample.addToIntent(intent);
+ startActivity(intent);
return true;
}
@@ -309,17 +316,12 @@ public class SampleChooserActivity extends AppCompatActivity
extension = reader.nextString();
break;
case "drm_scheme":
- Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
drmScheme = reader.nextString();
break;
case "drm_license_url":
- Assertions.checkState(!insidePlaylist,
- "Invalid attribute on nested item: drm_license_url");
drmLicenseUrl = reader.nextString();
break;
case "drm_key_request_properties":
- Assertions.checkState(!insidePlaylist,
- "Invalid attribute on nested item: drm_key_request_properties");
ArrayList drmKeyRequestPropertiesList = new ArrayList<>();
reader.beginObject();
while (reader.hasNext()) {
@@ -357,17 +359,21 @@ public class SampleChooserActivity extends AppCompatActivity
DrmInfo drmInfo =
drmScheme == null
? null
- : new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);
+ : new DrmInfo(
+ Util.getDrmUuid(drmScheme),
+ drmLicenseUrl,
+ drmKeyRequestProperties,
+ drmMultiSession);
if (playlistSamples != null) {
UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
- return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray);
+ return new PlaylistSample(sampleName, playlistSamplesArray);
} else {
return new UriSample(
sampleName,
drmInfo,
uri,
extension,
- adTagUri,
+ adTagUri != null ? Uri.parse(adTagUri) : null,
sphericalStereoMode);
}
}
@@ -497,116 +503,4 @@ public class SampleChooserActivity extends AppCompatActivity
}
}
-
- private static final class DrmInfo {
- public final String drmScheme;
- public final String drmLicenseUrl;
- public final String[] drmKeyRequestProperties;
- public final boolean drmMultiSession;
-
- public DrmInfo(
- String drmScheme,
- String drmLicenseUrl,
- String[] drmKeyRequestProperties,
- boolean drmMultiSession) {
- this.drmScheme = drmScheme;
- this.drmLicenseUrl = drmLicenseUrl;
- this.drmKeyRequestProperties = drmKeyRequestProperties;
- this.drmMultiSession = drmMultiSession;
- }
-
- public void updateIntent(Intent intent) {
- Assertions.checkNotNull(intent);
- intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme);
- intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl);
- intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties);
- intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession);
- }
- }
-
- private abstract static class Sample {
- public final String name;
- public final DrmInfo drmInfo;
-
- public Sample(String name, DrmInfo drmInfo) {
- this.name = name;
- this.drmInfo = drmInfo;
- }
-
- public Intent buildIntent(
- Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
- Intent intent = new Intent(context, PlayerActivity.class);
- intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders);
- intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
- if (drmInfo != null) {
- drmInfo.updateIntent(intent);
- }
- return intent;
- }
-
- }
-
- private static final class UriSample extends Sample {
-
- public final Uri uri;
- public final String extension;
- public final String adTagUri;
- public final String sphericalStereoMode;
-
- public UriSample(
- String name,
- DrmInfo drmInfo,
- Uri uri,
- String extension,
- String adTagUri,
- String sphericalStereoMode) {
- super(name, drmInfo);
- this.uri = uri;
- this.extension = extension;
- this.adTagUri = adTagUri;
- this.sphericalStereoMode = sphericalStereoMode;
- }
-
- @Override
- public Intent buildIntent(
- Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
- return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
- .setData(uri)
- .putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
- .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
- .putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode)
- .setAction(PlayerActivity.ACTION_VIEW);
- }
-
- }
-
- private static final class PlaylistSample extends Sample {
-
- public final UriSample[] children;
-
- public PlaylistSample(
- String name,
- DrmInfo drmInfo,
- UriSample... children) {
- super(name, drmInfo);
- this.children = children;
- }
-
- @Override
- public Intent buildIntent(
- Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
- String[] uris = new String[children.length];
- String[] extensions = new String[children.length];
- for (int i = 0; i < children.length; i++) {
- uris[i] = children[i].uri.toString();
- extensions[i] = children[i].extension;
- }
- return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
- .putExtra(PlayerActivity.URI_LIST_EXTRA, uris)
- .putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions)
- .setAction(PlayerActivity.ACTION_VIEW_LIST);
- }
-
- }
-
}
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
index d3e8b49b56..d6fe6e2dc1 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
@@ -306,7 +306,7 @@ public final class TrackSelectionDialog extends DialogFragment {
}
}
- /** Fragment to show a track seleciton in tab of the track selection dialog. */
+ /** Fragment to show a track selection in tab of the track selection dialog. */
public static final class TrackSelectionViewFragment extends Fragment
implements TrackSelectionView.TrackSelectionListener {
diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml
index 0729da2fc6..f74ce8c076 100644
--- a/demos/main/src/main/res/values/strings.xml
+++ b/demos/main/src/main/res/values/strings.xml
@@ -53,6 +53,8 @@
Playing sample without ads, as the IMA extension was not loaded
+ Playing sample without ads, as ads are not supported in concatenations
+
Failed to start downloadThis demo app does not support downloading playlists
diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle
index 4dc463ff81..4af8f94c58 100644
--- a/extensions/cast/build.gradle
+++ b/extensions/cast/build.gradle
@@ -31,13 +31,14 @@ android {
}
dependencies {
- api 'com.google.android.gms:play-services-cast-framework:16.1.2'
- implementation 'androidx.annotation:annotation:1.0.2'
+ api 'com.google.android.gms:play-services-cast-framework:17.0.0'
+ implementation 'androidx.annotation:annotation:1.1.0'
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
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 390deac933..6a33aa0428 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
@@ -45,8 +45,11 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
-import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* {@link Player} implementation that communicates with a Cast receiver app.
@@ -80,17 +83,18 @@ public final class CastPlayer extends BasePlayer {
private final CastTimelineTracker timelineTracker;
private final Timeline.Period period;
- private RemoteMediaClient remoteMediaClient;
-
// Result callbacks.
private final StatusListener statusListener;
private final SeekResultCallback seekResultCallback;
- // Listeners.
- private final CopyOnWriteArraySet listeners;
- private SessionAvailabilityListener sessionAvailabilityListener;
+ // Listeners and notification.
+ private final CopyOnWriteArrayList listeners;
+ private final ArrayList notificationsBatch;
+ private final ArrayDeque ongoingNotificationsTasks;
+ @Nullable private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state.
+ @Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection;
@@ -113,7 +117,9 @@ public final class CastPlayer extends BasePlayer {
period = new Timeline.Period();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
- listeners = new CopyOnWriteArraySet<>();
+ listeners = new CopyOnWriteArrayList<>();
+ notificationsBatch = new ArrayList<>();
+ ongoingNotificationsTasks = new ArrayDeque<>();
SessionManager sessionManager = castContext.getSessionManager();
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
@@ -141,6 +147,7 @@ public final class CastPlayer extends BasePlayer {
* starts at position 0.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
+ @Nullable
public PendingResult loadItem(MediaQueueItem item, long positionMs) {
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
}
@@ -156,8 +163,9 @@ public final class CastPlayer extends BasePlayer {
* @param repeatMode The repeat mode for the created media queue.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
- public PendingResult loadItems(MediaQueueItem[] items, int startIndex,
- long positionMs, @RepeatMode int repeatMode) {
+ @Nullable
+ public PendingResult loadItems(
+ MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
if (remoteMediaClient != null) {
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
waitingForInitialTimeline = true;
@@ -173,6 +181,7 @@ public final class CastPlayer extends BasePlayer {
* @param items The items to append.
* @return The Cast {@code PendingResult}, or null if no media queue exists.
*/
+ @Nullable
public PendingResult addItems(MediaQueueItem... items) {
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
}
@@ -187,6 +196,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
+ @Nullable
public PendingResult addItems(int periodId, MediaQueueItem... items) {
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
@@ -204,6 +214,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
+ @Nullable
public PendingResult removeItem(int periodId) {
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
return remoteMediaClient.queueRemoveItem(periodId, null);
@@ -222,6 +233,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
+ @Nullable
public PendingResult moveItem(int periodId, int newIndex) {
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
@@ -239,6 +251,7 @@ public final class CastPlayer extends BasePlayer {
* @return The item that corresponds to the period with the given id, or null if no media queue or
* period with id {@code periodId} exist.
*/
+ @Nullable
public MediaQueueItem getItem(int periodId) {
MediaStatus mediaStatus = getMediaStatus();
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
@@ -257,9 +270,9 @@ public final class CastPlayer extends BasePlayer {
/**
* Sets a listener for updates on the cast session availability.
*
- * @param listener The {@link SessionAvailabilityListener}.
+ * @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
*/
- public void setSessionAvailabilityListener(SessionAvailabilityListener listener) {
+ public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
sessionAvailabilityListener = listener;
}
@@ -296,12 +309,17 @@ public final class CastPlayer extends BasePlayer {
@Override
public void addListener(EventListener listener) {
- listeners.add(listener);
+ listeners.addIfAbsent(new ListenerHolder(listener));
}
@Override
public void removeListener(EventListener listener) {
- listeners.remove(listener);
+ for (ListenerHolder listenerHolder : listeners) {
+ if (listenerHolder.listener.equals(listener)) {
+ listenerHolder.release();
+ listeners.remove(listenerHolder);
+ }
+ }
}
@Override
@@ -311,6 +329,7 @@ public final class CastPlayer extends BasePlayer {
}
@Override
+ @Nullable
public ExoPlaybackException getPlaybackError() {
return null;
}
@@ -348,14 +367,13 @@ public final class CastPlayer extends BasePlayer {
pendingSeekCount++;
pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs;
- for (EventListener listener : listeners) {
- listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
- }
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
} else if (pendingSeekCount == 0) {
- for (EventListener listener : listeners) {
- listener.onSeekProcessed();
- }
+ notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
}
+ flushNotifications();
}
@Override
@@ -442,11 +460,6 @@ public final class CastPlayer extends BasePlayer {
return currentTimeline;
}
- @Override
- @Nullable public Object getCurrentManifest() {
- return null;
- }
-
@Override
public int getCurrentPeriodIndex() {
return getCurrentWindowIndex();
@@ -519,7 +532,7 @@ public final class CastPlayer extends BasePlayer {
// Internal methods.
- public void updateInternalState() {
+ private void updateInternalState() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return;
@@ -531,30 +544,40 @@ public final class CastPlayer extends BasePlayer {
|| this.playWhenReady != playWhenReady) {
this.playbackState = playbackState;
this.playWhenReady = playWhenReady;
- for (EventListener listener : listeners) {
- listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
- }
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState)));
}
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
- for (EventListener listener : listeners) {
- listener.onRepeatModeChanged(repeatMode);
- }
- }
- int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
- if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
- this.currentWindowIndex = currentWindowIndex;
- for (EventListener listener : listeners) {
- listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION);
- }
- }
- if (updateTracksAndSelections()) {
- for (EventListener listener : listeners) {
- listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
- }
+ notificationsBatch.add(
+ new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode)));
}
maybeUpdateTimelineAndNotify();
+
+ int currentWindowIndex = C.INDEX_UNSET;
+ MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
+ if (currentItem != null) {
+ currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());
+ }
+ if (currentWindowIndex == C.INDEX_UNSET) {
+ // The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
+ currentWindowIndex = 0;
+ }
+ if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
+ this.currentWindowIndex = currentWindowIndex;
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener ->
+ listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
+ }
+ if (updateTracksAndSelections()) {
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
+ }
+ flushNotifications();
}
private void maybeUpdateTimelineAndNotify() {
@@ -562,9 +585,9 @@ public final class CastPlayer extends BasePlayer {
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
waitingForInitialTimeline = false;
- for (EventListener listener : listeners) {
- listener.onTimelineChanged(currentTimeline, null, reason);
- }
+ notificationsBatch.add(
+ new ListenerNotificationTask(
+ listener -> listener.onTimelineChanged(currentTimeline, reason)));
}
}
@@ -654,7 +677,8 @@ public final class CastPlayer extends BasePlayer {
}
}
- private @Nullable MediaStatus getMediaStatus() {
+ @Nullable
+ private MediaStatus getMediaStatus() {
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
}
@@ -702,16 +726,6 @@ public final class CastPlayer extends BasePlayer {
}
}
- /**
- * Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
- * there is no media session, returns 0.
- */
- private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
- Integer currentItemId = mediaStatus != null
- ? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
- return currentItemId != null ? currentItemId : 0;
- }
-
private static boolean isTrackActive(long id, long[] activeTrackIds) {
for (long activeTrackId : activeTrackIds) {
if (activeTrackId == id) {
@@ -827,7 +841,23 @@ public final class CastPlayer extends BasePlayer {
}
- // Result callbacks hooks.
+ // Internal methods.
+
+ private void flushNotifications() {
+ boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
+ ongoingNotificationsTasks.addAll(notificationsBatch);
+ notificationsBatch.clear();
+ if (recursiveNotification) {
+ // This will be handled once the current notification task is finished.
+ return;
+ }
+ while (!ongoingNotificationsTasks.isEmpty()) {
+ ongoingNotificationsTasks.peekFirst().execute();
+ ongoingNotificationsTasks.removeFirst();
+ }
+ }
+
+ // Internal classes.
private final class SeekResultCallback implements ResultCallback {
@@ -841,9 +871,25 @@ public final class CastPlayer extends BasePlayer {
if (--pendingSeekCount == 0) {
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
- for (EventListener listener : listeners) {
- listener.onSeekProcessed();
- }
+ notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
+ flushNotifications();
+ }
+ }
+ }
+
+ private final class ListenerNotificationTask {
+
+ private final Iterator listenersSnapshot;
+ private final ListenerInvocation listenerInvocation;
+
+ private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
+ this.listenersSnapshot = listeners.iterator();
+ this.listenerInvocation = listenerInvocation;
+ }
+
+ public void execute() {
+ while (listenersSnapshot.hasNext()) {
+ listenersSnapshot.next().invoke(listenerInvocation);
}
}
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
index 800c19047b..b84f1c1f2b 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java
@@ -117,6 +117,7 @@ import java.util.Arrays;
Object tag = setTag ? ids[windowIndex] : null;
return window.set(
tag,
+ /* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic,
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
index d1660c3306..1dc25576a0 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.cast;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.gms.cast.CastStatusCodes;
@@ -33,7 +34,7 @@ import com.google.android.gms.cast.MediaTrack;
* @param mediaInfo The media info to get the duration from.
* @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable.
*/
- public static long getStreamDurationUs(MediaInfo mediaInfo) {
+ public static long getStreamDurationUs(@Nullable MediaInfo mediaInfo) {
if (mediaInfo == null) {
return C.TIME_UNSET;
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
index 06f0bec971..ebadb0a08a 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
@@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider;
+import java.util.Collections;
import java.util.List;
/**
@@ -27,16 +28,38 @@ import java.util.List;
*/
public final class DefaultCastOptionsProvider implements OptionsProvider {
+ /**
+ * App id of the Default Media Receiver app. Apps that do not require DRM support may use this
+ * receiver receiver app ID.
+ *
+ *
See https://developers.google.com/cast/docs/caf_receiver/#default_media_receiver.
+ */
+ public static final String APP_ID_DEFAULT_RECEIVER =
+ CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
+
+ /**
+ * App id for receiver app with rudimentary support for DRM.
+ *
+ *
This app id is only suitable for ExoPlayer's Cast Demo app, and it is not intended for
+ * production use. In order to use DRM, custom receiver apps should be used. For environments that
+ * do not require DRM, the default receiver app should be used (see {@link
+ * #APP_ID_DEFAULT_RECEIVER}).
+ */
+ // TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref:
+ // b/128603245].
+ public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273";
+
@Override
public CastOptions getCastOptions(Context context) {
return new CastOptions.Builder()
- .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
- .setStopReceiverApplicationWhenEndingSession(true).build();
+ .setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM)
+ .setStopReceiverApplicationWhenEndingSession(true)
+ .build();
}
@Override
public List getAdditionalSessionProviders(Context context) {
- return null;
+ return Collections.emptyList();
}
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java
new file mode 100644
index 0000000000..098803a512
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverter.java
@@ -0,0 +1,167 @@
+/*
+ * 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.ext.cast;
+
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaMetadata;
+import com.google.android.gms.cast.MediaQueueItem;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.UUID;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Default {@link MediaItemConverter} implementation. */
+public final class DefaultMediaItemConverter implements MediaItemConverter {
+
+ private static final String KEY_MEDIA_ITEM = "mediaItem";
+ private static final String KEY_PLAYER_CONFIG = "exoPlayerConfig";
+ private static final String KEY_URI = "uri";
+ private static final String KEY_TITLE = "title";
+ private static final String KEY_MIME_TYPE = "mimeType";
+ private static final String KEY_DRM_CONFIGURATION = "drmConfiguration";
+ private static final String KEY_UUID = "uuid";
+ private static final String KEY_LICENSE_URI = "licenseUri";
+ private static final String KEY_REQUEST_HEADERS = "requestHeaders";
+
+ @Override
+ public MediaItem toMediaItem(MediaQueueItem item) {
+ return getMediaItem(item.getMedia().getCustomData());
+ }
+
+ @Override
+ public MediaQueueItem toMediaQueueItem(MediaItem item) {
+ if (item.mimeType == null) {
+ throw new IllegalArgumentException("The item must specify its mimeType");
+ }
+ MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
+ if (item.title != null) {
+ metadata.putString(MediaMetadata.KEY_TITLE, item.title);
+ }
+ MediaInfo mediaInfo =
+ new MediaInfo.Builder(item.uri.toString())
+ .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
+ .setContentType(item.mimeType)
+ .setMetadata(metadata)
+ .setCustomData(getCustomData(item))
+ .build();
+ return new MediaQueueItem.Builder(mediaInfo).build();
+ }
+
+ // Deserialization.
+
+ private static MediaItem getMediaItem(JSONObject customData) {
+ try {
+ JSONObject mediaItemJson = customData.getJSONObject(KEY_MEDIA_ITEM);
+ MediaItem.Builder builder = new MediaItem.Builder();
+ builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
+ if (mediaItemJson.has(KEY_TITLE)) {
+ builder.setTitle(mediaItemJson.getString(KEY_TITLE));
+ }
+ if (mediaItemJson.has(KEY_MIME_TYPE)) {
+ builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE));
+ }
+ if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) {
+ builder.setDrmConfiguration(
+ getDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION)));
+ }
+ return builder.build();
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static DrmConfiguration getDrmConfiguration(JSONObject json) throws JSONException {
+ UUID uuid = UUID.fromString(json.getString(KEY_UUID));
+ Uri licenseUri = Uri.parse(json.getString(KEY_LICENSE_URI));
+ JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS);
+ HashMap requestHeaders = new HashMap<>();
+ for (Iterator iterator = requestHeadersJson.keys(); iterator.hasNext(); ) {
+ String key = iterator.next();
+ requestHeaders.put(key, requestHeadersJson.getString(key));
+ }
+ return new DrmConfiguration(uuid, licenseUri, requestHeaders);
+ }
+
+ // Serialization.
+
+ private static JSONObject getCustomData(MediaItem item) {
+ JSONObject json = new JSONObject();
+ try {
+ json.put(KEY_MEDIA_ITEM, getMediaItemJson(item));
+ JSONObject playerConfigJson = getPlayerConfigJson(item);
+ if (playerConfigJson != null) {
+ json.put(KEY_PLAYER_CONFIG, playerConfigJson);
+ }
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ return json;
+ }
+
+ private static JSONObject getMediaItemJson(MediaItem item) throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put(KEY_URI, item.uri.toString());
+ json.put(KEY_TITLE, item.title);
+ json.put(KEY_MIME_TYPE, item.mimeType);
+ if (item.drmConfiguration != null) {
+ json.put(KEY_DRM_CONFIGURATION, getDrmConfigurationJson(item.drmConfiguration));
+ }
+ return json;
+ }
+
+ private static JSONObject getDrmConfigurationJson(DrmConfiguration drmConfiguration)
+ throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put(KEY_UUID, drmConfiguration.uuid);
+ json.put(KEY_LICENSE_URI, drmConfiguration.licenseUri);
+ json.put(KEY_REQUEST_HEADERS, new JSONObject(drmConfiguration.requestHeaders));
+ return json;
+ }
+
+ @Nullable
+ private static JSONObject getPlayerConfigJson(MediaItem item) throws JSONException {
+ DrmConfiguration drmConfiguration = item.drmConfiguration;
+ if (drmConfiguration == null) {
+ return null;
+ }
+
+ String drmScheme;
+ if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) {
+ drmScheme = "widevine";
+ } else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) {
+ drmScheme = "playready";
+ } else {
+ return null;
+ }
+
+ JSONObject exoPlayerConfigJson = new JSONObject();
+ exoPlayerConfigJson.put("withCredentials", false);
+ exoPlayerConfigJson.put("protectionSystem", drmScheme);
+ if (drmConfiguration.licenseUri != null) {
+ exoPlayerConfigJson.put("licenseUrl", drmConfiguration.licenseUri);
+ }
+ if (!drmConfiguration.requestHeaders.isEmpty()) {
+ exoPlayerConfigJson.put("headers", new JSONObject(drmConfiguration.requestHeaders));
+ }
+
+ return exoPlayerConfigJson;
+ }
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java
index adb8e59070..7ac0da7078 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItem.java
@@ -17,42 +17,31 @@ package com.google.android.exoplayer2.ext.cast;
import android.net.Uri;
import androidx.annotation.Nullable;
-import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
-import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.UUID;
-import org.checkerframework.checker.initialization.qual.UnknownInitialization;
-import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
-/** Representation of an item that can be played by a media player. */
+/** Representation of a media item. */
public final class MediaItem {
/** A builder for {@link MediaItem} instances. */
public static final class Builder {
- @Nullable private UUID uuid;
- private String title;
- private String description;
- private MediaItem.UriBundle media;
- @Nullable private Object attachment;
- private List drmSchemes;
- private long startPositionUs;
- private long endPositionUs;
- private String mimeType;
+ @Nullable private Uri uri;
+ @Nullable private String title;
+ @Nullable private String mimeType;
+ @Nullable private DrmConfiguration drmConfiguration;
- /** Creates an builder with default field values. */
- public Builder() {
- clearInternal();
+ /** See {@link MediaItem#uri}. */
+ public Builder setUri(String uri) {
+ return setUri(Uri.parse(uri));
}
- /** See {@link MediaItem#uuid}. */
- public Builder setUuid(UUID uuid) {
- this.uuid = uuid;
+ /** See {@link MediaItem#uri}. */
+ public Builder setUri(Uri uri) {
+ this.uri = uri;
return this;
}
@@ -62,307 +51,125 @@ public final class MediaItem {
return this;
}
- /** See {@link MediaItem#description}. */
- public Builder setDescription(String description) {
- this.description = description;
- return this;
- }
-
- /** Equivalent to {@link #setMedia(UriBundle) setMedia(new UriBundle(Uri.parse(uri)))}. */
- public Builder setMedia(String uri) {
- return setMedia(new UriBundle(Uri.parse(uri)));
- }
-
- /** See {@link MediaItem#media}. */
- public Builder setMedia(UriBundle media) {
- this.media = media;
- return this;
- }
-
- /** See {@link MediaItem#attachment}. */
- public Builder setAttachment(Object attachment) {
- this.attachment = attachment;
- return this;
- }
-
- /** See {@link MediaItem#drmSchemes}. */
- public Builder setDrmSchemes(List drmSchemes) {
- this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes));
- return this;
- }
-
- /** See {@link MediaItem#startPositionUs}. */
- public Builder setStartPositionUs(long startPositionUs) {
- this.startPositionUs = startPositionUs;
- return this;
- }
-
- /** See {@link MediaItem#endPositionUs}. */
- public Builder setEndPositionUs(long endPositionUs) {
- Assertions.checkArgument(endPositionUs != C.TIME_END_OF_SOURCE);
- this.endPositionUs = endPositionUs;
- return this;
- }
-
/** See {@link MediaItem#mimeType}. */
public Builder setMimeType(String mimeType) {
this.mimeType = mimeType;
return this;
}
- /**
- * Equivalent to {@link #build()}, except it also calls {@link #clear()} after creating the
- * {@link MediaItem}.
- */
- public MediaItem buildAndClear() {
- MediaItem item = build();
- clearInternal();
- return item;
- }
-
- /** Returns the builder to default values. */
- public Builder clear() {
- clearInternal();
+ /** See {@link MediaItem#drmConfiguration}. */
+ public Builder setDrmConfiguration(DrmConfiguration drmConfiguration) {
+ this.drmConfiguration = drmConfiguration;
return this;
}
- /**
- * Returns a new {@link MediaItem} instance with the current builder values. This method also
- * clears any values passed to {@link #setUuid(UUID)}.
- */
+ /** Returns a new {@link MediaItem} instance with the current builder values. */
public MediaItem build() {
- UUID uuid = this.uuid;
- this.uuid = null;
- return new MediaItem(
- uuid != null ? uuid : UUID.randomUUID(),
- title,
- description,
- media,
- attachment,
- drmSchemes,
- startPositionUs,
- endPositionUs,
- mimeType);
- }
-
- @EnsuresNonNull({"title", "description", "media", "drmSchemes", "mimeType"})
- private void clearInternal(@UnknownInitialization Builder this) {
- uuid = null;
- title = "";
- description = "";
- media = UriBundle.EMPTY;
- attachment = null;
- drmSchemes = Collections.emptyList();
- startPositionUs = C.TIME_UNSET;
- endPositionUs = C.TIME_UNSET;
- mimeType = "";
+ Assertions.checkNotNull(uri);
+ return new MediaItem(uri, title, mimeType, drmConfiguration);
}
}
- /** Bundles a resource's URI with headers to attach to any request to that URI. */
- public static final class UriBundle {
-
- /** An empty {@link UriBundle}. */
- public static final UriBundle EMPTY = new UriBundle(Uri.EMPTY);
-
- /** A URI. */
- public final Uri uri;
-
- /** The headers to attach to any request for the given URI. */
- public final Map requestHeaders;
-
- /**
- * Creates an instance with no request headers.
- *
- * @param uri See {@link #uri}.
- */
- public UriBundle(Uri uri) {
- this(uri, Collections.emptyMap());
- }
-
- /**
- * Creates an instance with the given URI and request headers.
- *
- * @param uri See {@link #uri}.
- * @param requestHeaders See {@link #requestHeaders}.
- */
- public UriBundle(Uri uri, Map requestHeaders) {
- this.uri = uri;
- this.requestHeaders = Collections.unmodifiableMap(new HashMap<>(requestHeaders));
- }
-
- @Override
- public boolean equals(@Nullable Object other) {
- if (this == other) {
- return true;
- }
- if (other == null || getClass() != other.getClass()) {
- return false;
- }
-
- UriBundle uriBundle = (UriBundle) other;
- return uri.equals(uriBundle.uri) && requestHeaders.equals(uriBundle.requestHeaders);
- }
-
- @Override
- public int hashCode() {
- int result = uri.hashCode();
- result = 31 * result + requestHeaders.hashCode();
- return result;
- }
- }
-
- /**
- * Represents a DRM protection scheme, and optionally provides information about how to acquire
- * the license for the media.
- */
- public static final class DrmScheme {
+ /** DRM configuration for a media item. */
+ public static final class DrmConfiguration {
/** The UUID of the protection scheme. */
public final UUID uuid;
/**
- * Optional {@link UriBundle} for the license server. If no license server is provided, the
- * server must be provided by the media.
+ * Optional license server {@link Uri}. If {@code null} then the license server must be
+ * specified by the media.
*/
- @Nullable public final UriBundle licenseServer;
+ @Nullable public final Uri licenseUri;
+
+ /** Headers that should be attached to any license requests. */
+ public final Map requestHeaders;
/**
* Creates an instance.
*
* @param uuid See {@link #uuid}.
- * @param licenseServer See {@link #licenseServer}.
+ * @param licenseUri See {@link #licenseUri}.
+ * @param requestHeaders See {@link #requestHeaders}.
*/
- public DrmScheme(UUID uuid, @Nullable UriBundle licenseServer) {
+ public DrmConfiguration(
+ UUID uuid, @Nullable Uri licenseUri, @Nullable Map requestHeaders) {
this.uuid = uuid;
- this.licenseServer = licenseServer;
+ this.licenseUri = licenseUri;
+ this.requestHeaders =
+ requestHeaders == null
+ ? Collections.emptyMap()
+ : Collections.unmodifiableMap(requestHeaders);
}
@Override
- public boolean equals(@Nullable Object other) {
- if (this == other) {
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
return true;
}
- if (other == null || getClass() != other.getClass()) {
+ if (obj == null || getClass() != obj.getClass()) {
return false;
}
- DrmScheme drmScheme = (DrmScheme) other;
- return uuid.equals(drmScheme.uuid) && Util.areEqual(licenseServer, drmScheme.licenseServer);
+ DrmConfiguration other = (DrmConfiguration) obj;
+ return uuid.equals(other.uuid)
+ && Util.areEqual(licenseUri, other.licenseUri)
+ && requestHeaders.equals(other.requestHeaders);
}
@Override
public int hashCode() {
int result = uuid.hashCode();
- result = 31 * result + (licenseServer != null ? licenseServer.hashCode() : 0);
+ result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0);
+ result = 31 * result + requestHeaders.hashCode();
return result;
}
}
- /**
- * A UUID that identifies this item, potentially across different devices. The default value is
- * obtained by calling {@link UUID#randomUUID()}.
- */
- public final UUID uuid;
+ /** The media {@link Uri}. */
+ public final Uri uri;
- /** The title of the item. The default value is an empty string. */
- public final String title;
+ /** The title of the item, or {@code null} if unspecified. */
+ @Nullable public final String title;
- /** A description for the item. The default value is an empty string. */
- public final String description;
+ /** The mime type for the media, or {@code null} if unspecified. */
+ @Nullable public final String mimeType;
- /**
- * A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}.
- */
- public final UriBundle media;
+ /** Optional {@link DrmConfiguration} for the media. */
+ @Nullable public final DrmConfiguration drmConfiguration;
- /**
- * An optional opaque object to attach to the media item. Handling of this attachment is
- * implementation specific. The default value is null.
- */
- @Nullable public final Object attachment;
-
- /**
- * Immutable list of {@link DrmScheme} instances sorted in decreasing order of preference. The
- * default value is an empty list.
- */
- public final List drmSchemes;
-
- /**
- * The position in microseconds at which playback of this media item should start. {@link
- * C#TIME_UNSET} if playback should start at the default position. The default value is {@link
- * C#TIME_UNSET}.
- */
- public final long startPositionUs;
-
- /**
- * The position in microseconds at which playback of this media item should end. {@link
- * C#TIME_UNSET} if playback should end at the end of the media. The default value is {@link
- * C#TIME_UNSET}.
- */
- public final long endPositionUs;
-
- /**
- * The mime type of this media item. The default value is an empty string.
- *
- *
The usage of this mime type is optional and player implementation specific.
- */
- public final String mimeType;
-
- // TODO: Add support for sideloaded tracks, artwork, icon, and subtitle.
+ private MediaItem(
+ Uri uri,
+ @Nullable String title,
+ @Nullable String mimeType,
+ @Nullable DrmConfiguration drmConfiguration) {
+ this.uri = uri;
+ this.title = title;
+ this.mimeType = mimeType;
+ this.drmConfiguration = drmConfiguration;
+ }
@Override
- public boolean equals(@Nullable Object other) {
- if (this == other) {
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
return true;
}
- if (other == null || getClass() != other.getClass()) {
+ if (obj == null || getClass() != obj.getClass()) {
return false;
}
- MediaItem mediaItem = (MediaItem) other;
- return startPositionUs == mediaItem.startPositionUs
- && endPositionUs == mediaItem.endPositionUs
- && uuid.equals(mediaItem.uuid)
- && title.equals(mediaItem.title)
- && description.equals(mediaItem.description)
- && media.equals(mediaItem.media)
- && Util.areEqual(attachment, mediaItem.attachment)
- && drmSchemes.equals(mediaItem.drmSchemes)
- && mimeType.equals(mediaItem.mimeType);
+ MediaItem other = (MediaItem) obj;
+ return uri.equals(other.uri)
+ && Util.areEqual(title, other.title)
+ && Util.areEqual(mimeType, other.mimeType)
+ && Util.areEqual(drmConfiguration, other.drmConfiguration);
}
@Override
public int hashCode() {
- int result = uuid.hashCode();
- result = 31 * result + title.hashCode();
- result = 31 * result + description.hashCode();
- result = 31 * result + media.hashCode();
- result = 31 * result + (attachment != null ? attachment.hashCode() : 0);
- result = 31 * result + drmSchemes.hashCode();
- result = 31 * result + (int) (startPositionUs ^ (startPositionUs >>> 32));
- result = 31 * result + (int) (endPositionUs ^ (endPositionUs >>> 32));
- result = 31 * result + mimeType.hashCode();
+ int result = uri.hashCode();
+ result = 31 * result + (title == null ? 0 : title.hashCode());
+ result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
+ result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
return result;
}
-
- private MediaItem(
- UUID uuid,
- String title,
- String description,
- UriBundle media,
- @Nullable Object attachment,
- List drmSchemes,
- long startPositionUs,
- long endPositionUs,
- String mimeType) {
- this.uuid = uuid;
- this.title = title;
- this.description = description;
- this.media = media;
- this.attachment = attachment;
- this.drmSchemes = drmSchemes;
- this.startPositionUs = startPositionUs;
- this.endPositionUs = endPositionUs;
- this.mimeType = mimeType;
- }
}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java
new file mode 100644
index 0000000000..23633aa4d2
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemConverter.java
@@ -0,0 +1,38 @@
+/*
+ * 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.ext.cast;
+
+import com.google.android.gms.cast.MediaQueueItem;
+
+/** Converts between {@link MediaItem} and the Cast SDK's {@link MediaQueueItem}. */
+public interface MediaItemConverter {
+
+ /**
+ * Converts a {@link MediaItem} to a {@link MediaQueueItem}.
+ *
+ * @param mediaItem The {@link MediaItem}.
+ * @return An equivalent {@link MediaQueueItem}.
+ */
+ MediaQueueItem toMediaQueueItem(MediaItem mediaItem);
+
+ /**
+ * Converts a {@link MediaQueueItem} to a {@link MediaItem}.
+ *
+ * @param mediaQueueItem The {@link MediaQueueItem}.
+ * @return The equivalent {@link MediaItem}.
+ */
+ MediaItem toMediaItem(MediaQueueItem mediaQueueItem);
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java
deleted file mode 100644
index 184e347e1c..0000000000
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/MediaItemQueue.java
+++ /dev/null
@@ -1,85 +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.ext.cast;
-
-/** Represents a sequence of {@link MediaItem MediaItems}. */
-public interface MediaItemQueue {
-
- /**
- * Returns the item at the given index.
- *
- * @param index The index of the item to retrieve.
- * @return The item at the given index.
- * @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
- */
- MediaItem get(int index);
-
- /** Returns the number of items in this queue. */
- int getSize();
-
- /**
- * Appends the given sequence of items to the queue.
- *
- * @param items The sequence of items to append.
- */
- void add(MediaItem... items);
-
- /**
- * Adds the given sequence of items to the queue at the given position, so that the first of
- * {@code items} is placed at the given index.
- *
- * @param index The index at which {@code items} will be inserted.
- * @param items The sequence of items to append.
- * @throws IndexOutOfBoundsException If {@code index < 0 || index > getSize()}.
- */
- void add(int index, MediaItem... items);
-
- /**
- * Moves an existing item within the playlist.
- *
- *
Calling this method is equivalent to removing the item at position {@code indexFrom} and
- * immediately inserting it at position {@code indexTo}. If the moved item is being played at the
- * moment of the invocation, playback will stick with the moved item.
- *
- * @param indexFrom The index of the item to move.
- * @param indexTo The index at which the item will be placed after this operation.
- * @throws IndexOutOfBoundsException If for either index, {@code index < 0 || index >= getSize()}.
- */
- void move(int indexFrom, int indexTo);
-
- /**
- * Removes an item from the queue.
- *
- * @param index The index of the item to remove from the queue.
- * @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
- */
- void remove(int index);
-
- /**
- * Removes a range of items from the queue.
- *
- *
Does nothing if an empty range ({@code from == exclusiveTo}) is passed.
- *
- * @param from The inclusive index at which the range to remove starts.
- * @param exclusiveTo The exclusive index at which the range to remove ends.
- * @throws IndexOutOfBoundsException If {@code from < 0 || exclusiveTo > getSize() || from >
- * exclusiveTo}.
- */
- void removeRange(int from, int exclusiveTo);
-
- /** Removes all items in the queue. */
- void clear();
-}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java
new file mode 100644
index 0000000000..07055905a6
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.cast;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/cast/src/test/AndroidManifest.xml b/extensions/cast/src/test/AndroidManifest.xml
index aea8bda663..35a5150a47 100644
--- a/extensions/cast/src/test/AndroidManifest.xml
+++ b/extensions/cast/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java
new file mode 100644
index 0000000000..cf9b9d3496
--- /dev/null
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/DefaultMediaItemConverterTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.ext.cast;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
+import com.google.android.gms.cast.MediaQueueItem;
+import java.util.Collections;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test for {@link DefaultMediaItemConverter}. */
+@RunWith(AndroidJUnit4.class)
+public class DefaultMediaItemConverterTest {
+
+ @Test
+ public void serialize_deserialize_minimal() {
+ MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem item = builder.setUri(Uri.parse("http://example.com")).setMimeType("mime").build();
+
+ DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
+ MediaQueueItem queueItem = converter.toMediaQueueItem(item);
+ MediaItem reconstructedItem = converter.toMediaItem(queueItem);
+
+ assertThat(reconstructedItem).isEqualTo(item);
+ }
+
+ @Test
+ public void serialize_deserialize_complete() {
+ MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem item =
+ builder
+ .setUri(Uri.parse("http://example.com"))
+ .setTitle("title")
+ .setMimeType("mime")
+ .setDrmConfiguration(
+ new DrmConfiguration(
+ C.WIDEVINE_UUID,
+ Uri.parse("http://license.com"),
+ Collections.singletonMap("key", "value")))
+ .build();
+
+ DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
+ MediaQueueItem queueItem = converter.toMediaQueueItem(item);
+ MediaItem reconstructedItem = converter.toMediaItem(queueItem);
+
+ assertThat(reconstructedItem).isEqualTo(item);
+ }
+}
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java
index 9cdc073b06..7b410a8fbc 100644
--- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/MediaItemTest.java
@@ -21,10 +21,7 @@ import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.MimeTypes;
-import java.util.Arrays;
import java.util.HashMap;
-import java.util.List;
-import java.util.UUID;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -32,113 +29,58 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class MediaItemTest {
- @Test
- public void buildMediaItem_resetsUuid() {
- MediaItem.Builder builder = new MediaItem.Builder();
- UUID uuid = new UUID(1, 1);
- MediaItem item1 = builder.setUuid(uuid).build();
- MediaItem item2 = builder.build();
- MediaItem item3 = builder.build();
- assertThat(item1.uuid).isEqualTo(uuid);
- assertThat(item2.uuid).isNotEqualTo(uuid);
- assertThat(item3.uuid).isNotEqualTo(item2.uuid);
- assertThat(item3.uuid).isNotEqualTo(uuid);
- }
-
@Test
public void buildMediaItem_doesNotChangeState() {
MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item1 =
builder
- .setUuid(new UUID(0, 1))
- .setMedia("http://example.com")
+ .setUri(Uri.parse("http://example.com"))
.setTitle("title")
.setMimeType(MimeTypes.AUDIO_MP4)
- .setStartPositionUs(3)
- .setEndPositionUs(4)
.build();
- MediaItem item2 = builder.setUuid(new UUID(0, 1)).build();
+ MediaItem item2 = builder.build();
assertThat(item1).isEqualTo(item2);
}
- @Test
- public void buildMediaItem_assertDefaultValues() {
- assertDefaultValues(new MediaItem.Builder().build());
- }
-
- @Test
- public void buildAndClear_assertDefaultValues() {
- MediaItem.Builder builder = new MediaItem.Builder();
- builder
- .setMedia("http://example.com")
- .setTitle("title")
- .setMimeType(MimeTypes.AUDIO_MP4)
- .setStartPositionUs(3)
- .setEndPositionUs(4)
- .buildAndClear();
- assertDefaultValues(builder.build());
- }
-
@Test
public void equals_withEqualDrmSchemes_returnsTrue() {
- MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem.Builder builder1 = new MediaItem.Builder();
MediaItem mediaItem1 =
- builder
- .setUuid(new UUID(0, 1))
- .setMedia("www.google.com")
- .setDrmSchemes(createDummyDrmSchemes(1))
- .buildAndClear();
+ builder1
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(1))
+ .build();
+ MediaItem.Builder builder2 = new MediaItem.Builder();
MediaItem mediaItem2 =
- builder
- .setUuid(new UUID(0, 1))
- .setMedia("www.google.com")
- .setDrmSchemes(createDummyDrmSchemes(1))
- .buildAndClear();
+ builder2
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(1))
+ .build();
assertThat(mediaItem1).isEqualTo(mediaItem2);
}
@Test
public void equals_withDifferentDrmRequestHeaders_returnsFalse() {
- MediaItem.Builder builder = new MediaItem.Builder();
+ MediaItem.Builder builder1 = new MediaItem.Builder();
MediaItem mediaItem1 =
- builder
- .setUuid(new UUID(0, 1))
- .setMedia("www.google.com")
- .setDrmSchemes(createDummyDrmSchemes(1))
- .buildAndClear();
+ builder1
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(1))
+ .build();
+ MediaItem.Builder builder2 = new MediaItem.Builder();
MediaItem mediaItem2 =
- builder
- .setUuid(new UUID(0, 1))
- .setMedia("www.google.com")
- .setDrmSchemes(createDummyDrmSchemes(2))
- .buildAndClear();
+ builder2
+ .setUri(Uri.parse("www.google.com"))
+ .setDrmConfiguration(buildDrmConfiguration(2))
+ .build();
assertThat(mediaItem1).isNotEqualTo(mediaItem2);
}
- private static void assertDefaultValues(MediaItem item) {
- assertThat(item.title).isEmpty();
- assertThat(item.description).isEmpty();
- assertThat(item.media.uri).isEqualTo(Uri.EMPTY);
- assertThat(item.attachment).isNull();
- assertThat(item.drmSchemes).isEmpty();
- assertThat(item.startPositionUs).isEqualTo(C.TIME_UNSET);
- assertThat(item.endPositionUs).isEqualTo(C.TIME_UNSET);
- assertThat(item.mimeType).isEmpty();
- }
-
- private static List createDummyDrmSchemes(int seed) {
- HashMap requestHeaders1 = new HashMap<>();
- requestHeaders1.put("key1", "value1");
- requestHeaders1.put("key2", "value1");
- MediaItem.UriBundle uriBundle1 =
- new MediaItem.UriBundle(Uri.parse("www.uri1.com"), requestHeaders1);
- MediaItem.DrmScheme drmScheme1 = new MediaItem.DrmScheme(C.WIDEVINE_UUID, uriBundle1);
- HashMap requestHeaders2 = new HashMap<>();
- requestHeaders2.put("key3", "value3");
- requestHeaders2.put("key4", "valueWithSeed" + seed);
- MediaItem.UriBundle uriBundle2 =
- new MediaItem.UriBundle(Uri.parse("www.uri2.com"), requestHeaders2);
- MediaItem.DrmScheme drmScheme2 = new MediaItem.DrmScheme(C.PLAYREADY_UUID, uriBundle2);
- return Arrays.asList(drmScheme1, drmScheme2);
+ private static MediaItem.DrmConfiguration buildDrmConfiguration(int seed) {
+ HashMap requestHeaders = new HashMap<>();
+ requestHeaders.put("key1", "value1");
+ requestHeaders.put("key2", "value2" + seed);
+ return new MediaItem.DrmConfiguration(
+ C.WIDEVINE_UUID, Uri.parse("www.uri1.com"), requestHeaders);
}
}
diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle
index 76972a3530..9c49ba94e1 100644
--- a/extensions/cronet/build.gradle
+++ b/extensions/cronet/build.gradle
@@ -31,11 +31,13 @@ android {
}
dependencies {
- api 'org.chromium.net:cronet-embedded:73.3683.76'
+ api 'org.chromium.net:cronet-embedded:75.3770.101'
implementation project(modulePrefix + 'library-core')
- implementation 'androidx.annotation:annotation:1.0.2'
+ implementation 'androidx.annotation:annotation:1.1.0'
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'library')
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
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 ca196b1d2f..ed92523017 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
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.cronet;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.net.Uri;
import androidx.annotation.Nullable;
import android.text.TextUtils;
@@ -41,6 +43,7 @@ import java.util.Map.Entry;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.chromium.net.CronetEngine;
import org.chromium.net.CronetException;
import org.chromium.net.NetworkException;
@@ -113,16 +116,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
private final CronetEngine cronetEngine;
private final Executor executor;
- @Nullable private final Predicate contentTypePredicate;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
private final boolean handleSetCookieRequests;
- private final RequestProperties defaultRequestProperties;
+ @Nullable private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private final ConditionVariable operation;
private final Clock clock;
+ @Nullable private Predicate contentTypePredicate;
+
// Accessed by the calling thread only.
private boolean opened;
private long bytesToSkip;
@@ -130,18 +134,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
// to reads made by the Cronet thread.
- private UrlRequest currentUrlRequest;
- private DataSpec currentDataSpec;
+ @Nullable private UrlRequest currentUrlRequest;
+ @Nullable private DataSpec currentDataSpec;
// Reference written and read by calling thread only. Passed to Cronet thread as a local variable.
// operation.open() calls ensure writes into the buffer are visible to reads made by the calling
// thread.
- private ByteBuffer readBuffer;
+ @Nullable private ByteBuffer readBuffer;
// Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads
// made by the calling thread.
- private UrlResponseInfo responseInfo;
- private IOException exception;
+ @Nullable private UrlResponseInfo responseInfo;
+ @Nullable private IOException exception;
private boolean finished;
private volatile long currentConnectTimeoutMs;
@@ -155,7 +159,78 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* handling is a fast operation when using a direct executor.
*/
public CronetDataSource(CronetEngine cronetEngine, Executor executor) {
- this(cronetEngine, executor, /* contentTypePredicate= */ null);
+ this(
+ cronetEngine,
+ executor,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ /* resetTimeoutOnRedirects= */ false,
+ /* defaultRequestProperties= */ null);
+ }
+
+ /**
+ * @param cronetEngine A CronetEngine.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
+ * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
+ * hop from Cronet's internal network thread to the response handling thread. However, to
+ * avoid slowing down overall network performance, care must be taken to make sure response
+ * handling is a fast operation when using a direct executor.
+ * @param connectTimeoutMs The connection timeout, in milliseconds.
+ * @param readTimeoutMs The read timeout, in milliseconds.
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ */
+ public CronetDataSource(
+ CronetEngine cronetEngine,
+ Executor executor,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
+ @Nullable RequestProperties defaultRequestProperties) {
+ this(
+ cronetEngine,
+ executor,
+ connectTimeoutMs,
+ readTimeoutMs,
+ resetTimeoutOnRedirects,
+ Clock.DEFAULT,
+ defaultRequestProperties,
+ /* handleSetCookieRequests= */ false);
+ }
+
+ /**
+ * @param cronetEngine A CronetEngine.
+ * @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
+ * be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
+ * hop from Cronet's internal network thread to the response handling thread. However, to
+ * avoid slowing down overall network performance, care must be taken to make sure response
+ * handling is a fast operation when using a direct executor.
+ * @param connectTimeoutMs The connection timeout, in milliseconds.
+ * @param readTimeoutMs The read timeout, in milliseconds.
+ * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ * @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
+ * the redirect url in the "Cookie" header.
+ */
+ public CronetDataSource(
+ CronetEngine cronetEngine,
+ Executor executor,
+ int connectTimeoutMs,
+ int readTimeoutMs,
+ boolean resetTimeoutOnRedirects,
+ @Nullable RequestProperties defaultRequestProperties,
+ boolean handleSetCookieRequests) {
+ this(
+ cronetEngine,
+ executor,
+ connectTimeoutMs,
+ readTimeoutMs,
+ resetTimeoutOnRedirects,
+ Clock.DEFAULT,
+ defaultRequestProperties,
+ handleSetCookieRequests);
}
/**
@@ -168,7 +243,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
* #open(DataSpec)}.
+ * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link
+ * #setContentTypePredicate(Predicate)}.
*/
+ @Deprecated
public CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
@@ -179,9 +257,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
contentTypePredicate,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
- false,
- null,
- false);
+ /* resetTimeoutOnRedirects= */ false,
+ /* defaultRequestProperties= */ null);
}
/**
@@ -197,8 +274,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param defaultRequestProperties The default request properties to be used.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
+ * RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
*/
+ @Deprecated
public CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
@@ -206,7 +287,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
- RequestProperties defaultRequestProperties) {
+ @Nullable RequestProperties defaultRequestProperties) {
this(
cronetEngine,
executor,
@@ -214,9 +295,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
- Clock.DEFAULT,
defaultRequestProperties,
- false);
+ /* handleSetCookieRequests= */ false);
}
/**
@@ -232,10 +312,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
- * @param defaultRequestProperties The default request properties to be used.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
* the redirect url in the "Cookie" header.
+ * @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
+ * RequestProperties, boolean)} and {@link #setContentTypePredicate(Predicate)}.
*/
+ @Deprecated
public CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
@@ -243,35 +327,33 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
- RequestProperties defaultRequestProperties,
+ @Nullable RequestProperties defaultRequestProperties,
boolean handleSetCookieRequests) {
this(
cronetEngine,
executor,
- contentTypePredicate,
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
Clock.DEFAULT,
defaultRequestProperties,
handleSetCookieRequests);
+ this.contentTypePredicate = contentTypePredicate;
}
/* package */ CronetDataSource(
CronetEngine cronetEngine,
Executor executor,
- @Nullable Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
Clock clock,
- RequestProperties defaultRequestProperties,
+ @Nullable RequestProperties defaultRequestProperties,
boolean handleSetCookieRequests) {
super(/* isNetwork= */ true);
this.urlRequestCallback = new UrlRequestCallback();
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
this.executor = Assertions.checkNotNull(executor);
- this.contentTypePredicate = contentTypePredicate;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
@@ -282,6 +364,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
operation = new ConditionVariable();
}
+ /**
+ * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
+ * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
+ *
+ * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
+ * predicate that was previously set.
+ */
+ public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) {
+ this.contentTypePredicate = contentTypePredicate;
+ }
+
// HttpDataSource implementation.
@Override
@@ -305,6 +398,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
@Override
+ @Nullable
public Uri getUri() {
return responseInfo == null ? null : Uri.parse(responseInfo.getUrl());
}
@@ -317,22 +411,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
operation.close();
resetConnectTimeout();
currentDataSpec = dataSpec;
+ UrlRequest urlRequest;
try {
- currentUrlRequest = buildRequestBuilder(dataSpec).build();
+ urlRequest = buildRequestBuilder(dataSpec).build();
+ currentUrlRequest = urlRequest;
} catch (IOException e) {
- throw new OpenException(e, currentDataSpec, Status.IDLE);
+ throw new OpenException(e, dataSpec, Status.IDLE);
}
- currentUrlRequest.start();
+ urlRequest.start();
transferInitializing(dataSpec);
try {
boolean connectionOpened = blockUntilConnectTimeout();
if (exception != null) {
- throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));
+ throw new OpenException(exception, dataSpec, getStatus(urlRequest));
} else if (!connectionOpened) {
// The timeout was reached before the connection was opened.
- throw new OpenException(
- new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
+ throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(urlRequest));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
@@ -340,6 +435,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
// Check for a valid response code.
+ UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
int responseCode = responseInfo.getHttpStatusCode();
if (responseCode < 200 || responseCode > 299) {
InvalidResponseCodeException exception =
@@ -347,7 +443,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
responseCode,
responseInfo.getHttpStatusText(),
responseInfo.getAllHeaders(),
- currentDataSpec);
+ dataSpec);
if (responseCode == 416) {
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
}
@@ -355,11 +451,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
// Check for a valid content type.
+ Predicate contentTypePredicate = this.contentTypePredicate;
if (contentTypePredicate != null) {
List contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
- if (!contentTypePredicate.evaluate(contentType)) {
- throw new InvalidContentTypeException(contentType, currentDataSpec);
+ if (contentType != null && !contentTypePredicate.evaluate(contentType)) {
+ throw new InvalidContentTypeException(contentType, dataSpec);
}
}
@@ -369,7 +466,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Calculate the content length.
- if (!getIsCompressed(responseInfo)) {
+ if (!isCompressed(responseInfo)) {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;
} else {
@@ -378,7 +475,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
} else {
// If the response is compressed then the content length will be that of the compressed data
// which isn't what we want. Always use the dataSpec length in this case.
- bytesRemaining = currentDataSpec.length;
+ bytesRemaining = dataSpec.length;
}
opened = true;
@@ -397,37 +494,19 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return C.RESULT_END_OF_INPUT;
}
+ ByteBuffer readBuffer = this.readBuffer;
if (readBuffer == null) {
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
readBuffer.limit(0);
+ this.readBuffer = readBuffer;
}
while (!readBuffer.hasRemaining()) {
// Fill readBuffer with more data from Cronet.
operation.close();
readBuffer.clear();
- currentUrlRequest.read(readBuffer);
- try {
- if (!operation.block(readTimeoutMs)) {
- throw new SocketTimeoutException();
- }
- } catch (InterruptedException e) {
- // The operation is ongoing so replace readBuffer to avoid it being written to by this
- // operation during a subsequent request.
- readBuffer = null;
- Thread.currentThread().interrupt();
- throw new HttpDataSourceException(
- new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ);
- } catch (SocketTimeoutException e) {
- // The operation is ongoing so replace readBuffer to avoid it being written to by this
- // operation during a subsequent request.
- readBuffer = null;
- throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ);
- }
+ readInternal(castNonNull(readBuffer));
- if (exception != null) {
- throw new HttpDataSourceException(exception, currentDataSpec,
- HttpDataSourceException.TYPE_READ);
- } else if (finished) {
+ if (finished) {
bytesRemaining = 0;
return C.RESULT_END_OF_INPUT;
} else {
@@ -452,6 +531,115 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return bytesRead;
}
+ /**
+ * Reads up to {@code buffer.remaining()} bytes of data and stores them into {@code buffer},
+ * starting at {@code buffer.position()}. Advances the position of the buffer by the number of
+ * bytes read and returns this length.
+ *
+ *
If there is an error, a {@link HttpDataSourceException} is thrown and the contents of {@code
+ * buffer} should be ignored. If the exception has error code {@code
+ * HttpDataSourceException.TYPE_READ}, note that Cronet may continue writing into {@code buffer}
+ * after the method has returned. Thus the caller should not attempt to reuse the buffer.
+ *
+ *
If {@code buffer.remaining()} is zero then 0 is returned. Otherwise, if no data is available
+ * because the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is
+ * returned. Otherwise, the call will block until at least one byte of data has been read and the
+ * number of bytes read is returned.
+ *
+ *
Passed buffer must be direct ByteBuffer. If you have a non-direct ByteBuffer, consider the
+ * alternative read method with its backed array.
+ *
+ * @param buffer The ByteBuffer into which the read data should be stored. Must be a direct
+ * ByteBuffer.
+ * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available
+ * because the end of the opened range has been reached.
+ * @throws HttpDataSourceException If an error occurs reading from the source.
+ * @throws IllegalArgumentException If {@code buffer} is not a direct ByteBuffer.
+ */
+ public int read(ByteBuffer buffer) throws HttpDataSourceException {
+ Assertions.checkState(opened);
+
+ if (!buffer.isDirect()) {
+ throw new IllegalArgumentException("Passed buffer is not a direct ByteBuffer");
+ }
+ if (!buffer.hasRemaining()) {
+ return 0;
+ } else if (bytesRemaining == 0) {
+ return C.RESULT_END_OF_INPUT;
+ }
+ int readLength = buffer.remaining();
+
+ if (readBuffer != null) {
+ // Skip all the bytes we can from readBuffer if there are still bytes to skip.
+ if (bytesToSkip != 0) {
+ if (bytesToSkip >= readBuffer.remaining()) {
+ bytesToSkip -= readBuffer.remaining();
+ readBuffer.position(readBuffer.limit());
+ } else {
+ readBuffer.position(readBuffer.position() + (int) bytesToSkip);
+ bytesToSkip = 0;
+ }
+ }
+
+ // If there is existing data in the readBuffer, read as much as possible. Return if any read.
+ int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer);
+ if (copyBytes != 0) {
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= copyBytes;
+ }
+ bytesTransferred(copyBytes);
+ return copyBytes;
+ }
+ }
+
+ boolean readMore = true;
+ while (readMore) {
+ // If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's
+ // buffer. If we do not need to skip bytes, we may write to buffer directly.
+ final boolean useCallerBuffer = bytesToSkip == 0;
+
+ operation.close();
+
+ if (!useCallerBuffer) {
+ if (readBuffer == null) {
+ readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
+ } else {
+ readBuffer.clear();
+ }
+ if (bytesToSkip < READ_BUFFER_SIZE_BYTES) {
+ readBuffer.limit((int) bytesToSkip);
+ }
+ }
+
+ // Fill buffer with more data from Cronet.
+ readInternal(useCallerBuffer ? buffer : castNonNull(readBuffer));
+
+ if (finished) {
+ bytesRemaining = 0;
+ return C.RESULT_END_OF_INPUT;
+ } else {
+ // The operation didn't time out, fail or finish, and therefore data must have been read.
+ Assertions.checkState(
+ useCallerBuffer
+ ? readLength > buffer.remaining()
+ : castNonNull(readBuffer).position() > 0);
+ // If we meant to skip bytes, subtract what was left and repeat, otherwise, continue.
+ if (useCallerBuffer) {
+ readMore = false;
+ } else {
+ bytesToSkip -= castNonNull(readBuffer).position();
+ }
+ }
+ }
+
+ final int bytesRead = readLength - buffer.remaining();
+ if (bytesRemaining != C.LENGTH_UNSET) {
+ bytesRemaining -= bytesRead;
+ }
+ bytesTransferred(bytesRead);
+ return bytesRead;
+ }
+
@Override
public synchronized void close() {
if (currentUrlRequest != null) {
@@ -524,7 +712,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
requestBuilder.addHeader("Range", rangeValue.toString());
}
- // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=767025 is fixed
+ // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed
// (adjusting the code as necessary).
// Force identity encoding unless gzip is allowed.
// if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
@@ -553,7 +741,49 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
}
- private static boolean getIsCompressed(UrlResponseInfo info) {
+ /**
+ * Reads up to {@code buffer.remaining()} bytes of data from {@code currentUrlRequest} and stores
+ * them into {@code buffer}. If there is an error and {@code buffer == readBuffer}, then it resets
+ * the current {@code readBuffer} object so that it is not reused in the future.
+ *
+ * @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer.
+ * @throws HttpDataSourceException If an error occurs reading from the source.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private void readInternal(ByteBuffer buffer) throws HttpDataSourceException {
+ castNonNull(currentUrlRequest).read(buffer);
+ try {
+ if (!operation.block(readTimeoutMs)) {
+ throw new SocketTimeoutException();
+ }
+ } catch (InterruptedException e) {
+ // The operation is ongoing so replace buffer to avoid it being written to by this
+ // operation during a subsequent request.
+ if (buffer == readBuffer) {
+ readBuffer = null;
+ }
+ Thread.currentThread().interrupt();
+ throw new HttpDataSourceException(
+ new InterruptedIOException(e),
+ castNonNull(currentDataSpec),
+ HttpDataSourceException.TYPE_READ);
+ } catch (SocketTimeoutException e) {
+ // The operation is ongoing so replace buffer to avoid it being written to by this
+ // operation during a subsequent request.
+ if (buffer == readBuffer) {
+ readBuffer = null;
+ }
+ throw new HttpDataSourceException(
+ e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
+ }
+
+ if (exception != null) {
+ throw new HttpDataSourceException(
+ exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
+ }
+ }
+
+ private static boolean isCompressed(UrlResponseInfo info) {
for (Map.Entry entry : info.getAllHeadersAsList()) {
if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
return !entry.getValue().equalsIgnoreCase("identity");
@@ -631,10 +861,22 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
return statusHolder[0];
}
- private static boolean isEmpty(List> list) {
+ @EnsuresNonNullIf(result = false, expression = "#1")
+ private static boolean isEmpty(@Nullable List> list) {
return list == null || list.isEmpty();
}
+ // Copy as much as possible from the src buffer into dst buffer.
+ // Returns the number of bytes copied.
+ private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) {
+ int remaining = Math.min(src.remaining(), dst.remaining());
+ int limit = src.limit();
+ src.limit(src.position() + remaining);
+ dst.put(src);
+ src.limit(limit);
+ return remaining;
+ }
+
private final class UrlRequestCallback extends UrlRequest.Callback {
@Override
@@ -643,13 +885,15 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (request != currentUrlRequest) {
return;
}
- if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
+ UrlRequest urlRequest = Assertions.checkNotNull(currentUrlRequest);
+ DataSpec dataSpec = Assertions.checkNotNull(currentDataSpec);
+ if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
int responseCode = info.getHttpStatusCode();
// The industry standard is to disregard POST redirects when the status code is 307 or 308.
if (responseCode == 307 || responseCode == 308) {
exception =
new InvalidResponseCodeException(
- responseCode, info.getHttpStatusText(), info.getAllHeaders(), currentDataSpec);
+ responseCode, info.getHttpStatusText(), info.getAllHeaders(), dataSpec);
operation.open();
return;
}
@@ -658,40 +902,46 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
resetConnectTimeout();
}
- Map> headers = info.getAllHeaders();
- if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
+ if (!handleSetCookieRequests) {
request.followRedirect();
- } else {
- currentUrlRequest.cancel();
- DataSpec redirectUrlDataSpec;
- if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
- // For POST redirects that aren't 307 or 308, the redirect is followed but request is
- // transformed into a GET.
- redirectUrlDataSpec =
- new DataSpec(
- Uri.parse(newLocationUrl),
- DataSpec.HTTP_METHOD_GET,
- /* httpBody= */ null,
- currentDataSpec.absoluteStreamPosition,
- currentDataSpec.position,
- currentDataSpec.length,
- currentDataSpec.key,
- currentDataSpec.flags);
- } else {
- redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl));
- }
- UrlRequest.Builder requestBuilder;
- try {
- requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
- } catch (IOException e) {
- exception = e;
- return;
- }
- String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
- attachCookies(requestBuilder, cookieHeadersValue);
- currentUrlRequest = requestBuilder.build();
- currentUrlRequest.start();
+ return;
}
+
+ List setCookieHeaders = info.getAllHeaders().get(SET_COOKIE);
+ if (isEmpty(setCookieHeaders)) {
+ request.followRedirect();
+ return;
+ }
+
+ urlRequest.cancel();
+ DataSpec redirectUrlDataSpec;
+ if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
+ // For POST redirects that aren't 307 or 308, the redirect is followed but request is
+ // transformed into a GET.
+ redirectUrlDataSpec =
+ new DataSpec(
+ Uri.parse(newLocationUrl),
+ DataSpec.HTTP_METHOD_GET,
+ /* httpBody= */ null,
+ dataSpec.absoluteStreamPosition,
+ dataSpec.position,
+ dataSpec.length,
+ dataSpec.key,
+ dataSpec.flags);
+ } else {
+ redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
+ }
+ UrlRequest.Builder requestBuilder;
+ try {
+ requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
+ } catch (IOException e) {
+ exception = e;
+ return;
+ }
+ String cookieHeadersValue = parseCookies(setCookieHeaders);
+ attachCookies(requestBuilder, cookieHeadersValue);
+ currentUrlRequest = requestBuilder.build();
+ currentUrlRequest.start();
}
@Override
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
index 93edb4e893..4086011b4f 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
@@ -20,9 +20,7 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
-import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException;
import com.google.android.exoplayer2.upstream.TransferListener;
-import com.google.android.exoplayer2.util.Predicate;
import java.util.concurrent.Executor;
import org.chromium.net.CronetEngine;
@@ -45,8 +43,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
private final CronetEngineWrapper cronetEngineWrapper;
private final Executor executor;
- private final Predicate contentTypePredicate;
- private final @Nullable TransferListener transferListener;
+ @Nullable private final TransferListener transferListener;
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
@@ -64,21 +61,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
* suitable CronetEngine can be build.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
HttpDataSource.Factory fallbackFactory) {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
@@ -98,20 +90,15 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
String userAgent) {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
@@ -132,9 +119,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
@@ -143,7 +127,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
@@ -151,7 +134,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
@@ -172,9 +154,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
@@ -184,7 +163,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
@@ -192,7 +170,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
this(
cronetEngineWrapper,
executor,
- contentTypePredicate,
/* transferListener= */ null,
connectTimeoutMs,
readTimeoutMs,
@@ -212,9 +189,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
* suitable CronetEngine can be build.
@@ -222,11 +196,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
HttpDataSource.Factory fallbackFactory) {
- this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
+ this(
+ cronetEngineWrapper,
+ executor,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false,
+ fallbackFactory);
}
/**
@@ -241,22 +220,27 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
String userAgent) {
- this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
- new DefaultHttpDataSourceFactory(userAgent, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));
+ this(
+ cronetEngineWrapper,
+ executor,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false,
+ new DefaultHttpDataSourceFactory(
+ userAgent,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ false));
}
/**
@@ -267,9 +251,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
@@ -279,16 +260,20 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
int connectTimeoutMs,
int readTimeoutMs,
boolean resetTimeoutOnRedirects,
String userAgent) {
- this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
- new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
- readTimeoutMs, resetTimeoutOnRedirects));
+ this(
+ cronetEngineWrapper,
+ executor,
+ transferListener,
+ DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS,
+ resetTimeoutOnRedirects,
+ new DefaultHttpDataSourceFactory(
+ userAgent, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects));
}
/**
@@ -299,9 +284,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
- * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
- * predicate then an {@link InvalidContentTypeException} is thrown from {@link
- * CronetDataSource#open}.
* @param transferListener An optional listener.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
@@ -312,7 +294,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
- Predicate contentTypePredicate,
@Nullable TransferListener transferListener,
int connectTimeoutMs,
int readTimeoutMs,
@@ -320,7 +301,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
HttpDataSource.Factory fallbackFactory) {
this.cronetEngineWrapper = cronetEngineWrapper;
this.executor = executor;
- this.contentTypePredicate = contentTypePredicate;
this.transferListener = transferListener;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
@@ -339,7 +319,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
new CronetDataSource(
cronetEngine,
executor,
- contentTypePredicate,
connectTimeoutMs,
readTimeoutMs,
resetTimeoutOnRedirects,
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
index 270c1f6323..7d549be7cb 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.cronet;
import android.content.Context;
import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
@@ -37,8 +38,8 @@ public final class CronetEngineWrapper {
private static final String TAG = "CronetEngineWrapper";
- private final CronetEngine cronetEngine;
- private final @CronetEngineSource int cronetEngineSource;
+ @Nullable private final CronetEngine cronetEngine;
+ @CronetEngineSource private final int cronetEngineSource;
/**
* Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link
@@ -144,7 +145,8 @@ public final class CronetEngineWrapper {
*
* @return A {@link CronetEngineSource} value.
*/
- public @CronetEngineSource int getCronetEngineSource() {
+ @CronetEngineSource
+ public int getCronetEngineSource() {
return cronetEngineSource;
}
@@ -153,13 +155,14 @@ public final class CronetEngineWrapper {
*
* @return The CronetEngine, or null if no CronetEngine is available.
*/
+ @Nullable
/* package */ CronetEngine getCronetEngine() {
return cronetEngine;
}
private static class CronetProviderComparator implements Comparator {
- private final String gmsCoreCronetName;
+ @Nullable private final String gmsCoreCronetName;
private final boolean preferGMSCoreCronet;
// Multi-catch can only be used for API 19+ in this case.
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java
new file mode 100644
index 0000000000..ec0cf8df05
--- /dev/null
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.cronet;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/cronet/src/test/AndroidManifest.xml b/extensions/cronet/src/test/AndroidManifest.xml
index 82cffe17c2..d6e09107a7 100644
--- a/extensions/cronet/src/test/AndroidManifest.xml
+++ b/extensions/cronet/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
index 7c4c03dd87..2be369bad9 100644
--- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
+++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
@@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.cronet;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
@@ -38,7 +38,6 @@ import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Clock;
-import com.google.android.exoplayer2.util.Predicate;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.net.SocketTimeoutException;
@@ -85,7 +84,6 @@ public final class CronetDataSourceTest {
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
@Mock private UrlRequest mockUrlRequest;
- @Mock private Predicate mockContentTypePredicate;
@Mock private TransferListener mockTransferListener;
@Mock private Executor mockExecutor;
@Mock private NetworkException mockNetworkException;
@@ -95,21 +93,19 @@ public final class CronetDataSourceTest {
private boolean redirectCalled;
@Before
- public void setUp() throws Exception {
+ public void setUp() {
MockitoAnnotations.initMocks(this);
dataSourceUnderTest =
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
+ /* resetTimeoutOnRedirects= */ true,
Clock.DEFAULT,
- null,
- false);
+ /* defaultRequestProperties= */ null,
+ /* handleSetCookieRequests= */ false);
dataSourceUnderTest.addTransferListener(mockTransferListener);
- when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
.thenReturn(mockUrlRequestBuilder);
@@ -283,7 +279,13 @@ public final class CronetDataSourceTest {
@Test
public void testRequestOpenValidatesContentTypePredicate() {
mockResponseStartSuccess();
- when(mockContentTypePredicate.evaluate(anyString())).thenReturn(false);
+
+ ArrayList testedContentTypes = new ArrayList<>();
+ dataSourceUnderTest.setContentTypePredicate(
+ (String input) -> {
+ testedContentTypes.add(input);
+ return false;
+ });
try {
dataSourceUnderTest.open(testDataSpec);
@@ -292,7 +294,8 @@ public final class CronetDataSourceTest {
assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
- verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE);
+ assertThat(testedContentTypes).hasSize(1);
+ assertThat(testedContentTypes.get(0)).isEqualTo(TEST_CONTENT_TYPE);
}
}
@@ -551,6 +554,260 @@ public final class CronetDataSourceTest {
assertThat(bytesRead).isEqualTo(16);
}
+ @Test
+ public void testRequestReadByteBufferTwice() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(8);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+
+ // Use a wrapped ByteBuffer instead of direct for coverage.
+ returnedBuffer.rewind();
+ bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 8));
+ assertThat(bytesRead).isEqualTo(8);
+
+ // Separate cronet calls for each read.
+ verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
+ verify(mockTransferListener, times(2))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ }
+
+ @Test
+ public void testRequestIntermixRead() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ // Chunking reads into parts 6, 7, 8, 9.
+ mockReadSuccess(0, 30);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(6);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 6));
+ assertThat(bytesRead).isEqualTo(6);
+
+ byte[] returnedBytes = new byte[7];
+ bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 7);
+ assertThat(returnedBytes).isEqualTo(buildTestDataArray(6, 7));
+ assertThat(bytesRead).isEqualTo(6 + 7);
+
+ returnedBuffer = ByteBuffer.allocateDirect(8);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(13, 8));
+ assertThat(bytesRead).isEqualTo(6 + 7 + 8);
+
+ returnedBytes = new byte[9];
+ bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 9);
+ assertThat(returnedBytes).isEqualTo(buildTestDataArray(21, 9));
+ assertThat(bytesRead).isEqualTo(6 + 7 + 8 + 9);
+
+ // First ByteBuffer call. The first byte[] call populates enough bytes for the rest.
+ verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 7);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 9);
+ }
+
+ @Test
+ public void testSecondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ testResponseHeader.put("Content-Length", Long.toString(1L));
+ mockReadSuccess(0, 16);
+
+ // First request.
+ dataSourceUnderTest.open(testDataSpec);
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ dataSourceUnderTest.read(returnedBuffer);
+ dataSourceUnderTest.close();
+
+ testResponseHeader.remove("Content-Length");
+ mockReadSuccess(0, 16);
+
+ // Second request.
+ dataSourceUnderTest.open(testDataSpec);
+ returnedBuffer = ByteBuffer.allocateDirect(16);
+ returnedBuffer.limit(10);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(10);
+ returnedBuffer.limit(returnedBuffer.capacity());
+ bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(6);
+ returnedBuffer.rewind();
+ bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
+ }
+
+ @Test
+ public void testRangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(1000, 5000);
+ testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
+ testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
+ }
+
+ @Test
+ public void testRangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException {
+ // Tests for skipping bytes.
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 7000);
+ testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
+ testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
+ }
+
+ @Test
+ public void testReadByteBufferWithUnsetLength() throws HttpDataSourceException {
+ testResponseHeader.remove("Content-Length");
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ returnedBuffer.limit(8);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+ assertThat(bytesRead).isEqualTo(8);
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ }
+
+ @Test
+ public void testReadByteBufferReturnsWhatItCan() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(24);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 16));
+ assertThat(bytesRead).isEqualTo(16);
+ verify(mockTransferListener)
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
+ }
+
+ @Test
+ public void testOverreadByteBuffer() throws HttpDataSourceException {
+ testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);
+ testResponseHeader.put("Content-Length", Long.toString(16L));
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ int bytesRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(8);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+
+ // The current buffer is kept if not completely consumed by DataSource reader.
+ returnedBuffer = ByteBuffer.allocateDirect(6);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(14);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 6));
+
+ // 2 bytes left at this point.
+ returnedBuffer = ByteBuffer.allocateDirect(8);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(14, 2));
+
+ // Called on each.
+ verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
+ verify(mockTransferListener, times(1))
+ .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);
+
+ // Now we already returned the 16 bytes initially asked.
+ // Try to read again even though all requested 16 bytes are already returned.
+ // Return C.RESULT_END_OF_INPUT
+ returnedBuffer = ByteBuffer.allocateDirect(16);
+ int bytesOverRead = dataSourceUnderTest.read(returnedBuffer);
+ assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
+ assertThat(returnedBuffer.position()).isEqualTo(0);
+ // C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
+ verify(mockTransferListener, never())
+ .onBytesTransferred(
+ dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);
+ // Number of calls to cronet should not have increased.
+ verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
+ // Check for connection not automatically closed.
+ verify(mockUrlRequest, never()).cancel();
+ assertThat(bytesRead).isEqualTo(16);
+ }
+
+ @Test
+ public void testClosedMeansClosedReadByteBuffer() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadSuccess(0, 16);
+
+ int bytesRead = 0;
+ dataSourceUnderTest.open(testDataSpec);
+
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
+ returnedBuffer.limit(8);
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ returnedBuffer.flip();
+ assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
+ assertThat(bytesRead).isEqualTo(8);
+
+ dataSourceUnderTest.close();
+ verify(mockTransferListener)
+ .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
+
+ try {
+ bytesRead += dataSourceUnderTest.read(returnedBuffer);
+ fail();
+ } catch (IllegalStateException e) {
+ // Expected.
+ }
+
+ // 16 bytes were attempted but only 8 should have been successfully read.
+ assertThat(bytesRead).isEqualTo(8);
+ }
+
@Test
public void testConnectTimeout() throws InterruptedException {
long startTimeMs = SystemClock.elapsedRealtime();
@@ -734,7 +991,6 @@ public final class CronetDataSourceTest {
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects
@@ -765,13 +1021,12 @@ public final class CronetDataSourceTest {
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
+ /* resetTimeoutOnRedirects= */ true,
Clock.DEFAULT,
- null,
- true);
+ /* defaultRequestProperties= */ null,
+ /* handleSetCookieRequests= */ true);
dataSourceUnderTest.addTransferListener(mockTransferListener);
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
@@ -804,13 +1059,12 @@ public final class CronetDataSourceTest {
new CronetDataSource(
mockCronetEngine,
mockExecutor,
- mockContentTypePredicate,
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
- true, // resetTimeoutOnRedirects
+ /* resetTimeoutOnRedirects= */ true,
Clock.DEFAULT,
- null,
- true);
+ /* defaultRequestProperties= */ null,
+ /* handleSetCookieRequests= */ true);
dataSourceUnderTest.addTransferListener(mockTransferListener);
mockSingleRedirectSuccess();
mockFollowRedirectSuccess();
@@ -855,6 +1109,36 @@ public final class CronetDataSourceTest {
}
}
+ @Test
+ public void testReadByteBufferFailure() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadFailure();
+
+ dataSourceUnderTest.open(testDataSpec);
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ try {
+ dataSourceUnderTest.read(returnedBuffer);
+ fail("dataSourceUnderTest.read() returned, but IOException expected");
+ } catch (IOException e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void testReadNonDirectedByteBufferFailure() throws HttpDataSourceException {
+ mockResponseStartSuccess();
+ mockReadFailure();
+
+ dataSourceUnderTest.open(testDataSpec);
+ byte[] returnedBuffer = new byte[8];
+ try {
+ dataSourceUnderTest.read(ByteBuffer.wrap(returnedBuffer));
+ fail("dataSourceUnderTest.read() returned, but IllegalArgumentException expected");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
@Test
public void testReadInterrupted() throws HttpDataSourceException, InterruptedException {
mockResponseStartSuccess();
@@ -886,6 +1170,37 @@ public final class CronetDataSourceTest {
timedOutLatch.await();
}
+ @Test
+ public void testReadByteBufferInterrupted() throws HttpDataSourceException, InterruptedException {
+ mockResponseStartSuccess();
+ dataSourceUnderTest.open(testDataSpec);
+
+ final ConditionVariable startCondition = buildReadStartedCondition();
+ final CountDownLatch timedOutLatch = new CountDownLatch(1);
+ ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
+ Thread thread =
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ dataSourceUnderTest.read(returnedBuffer);
+ fail();
+ } catch (HttpDataSourceException e) {
+ // Expected.
+ assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
+ timedOutLatch.countDown();
+ }
+ }
+ };
+ thread.start();
+ startCondition.block();
+
+ assertNotCountedDown(timedOutLatch);
+ // Now we interrupt.
+ thread.interrupt();
+ timedOutLatch.await();
+ }
+
@Test
public void testAllowDirectExecutor() throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
@@ -1064,4 +1379,17 @@ public final class CronetDataSourceTest {
testBuffer.flip();
return testBuffer;
}
+
+ // Returns a copy of what is remaining in the src buffer from the current position to capacity.
+ private static byte[] copyByteBufferToArray(ByteBuffer src) {
+ if (src == null) {
+ return null;
+ }
+ byte[] copy = new byte[src.remaining()];
+ int index = 0;
+ while (src.hasRemaining()) {
+ copy[index++] = src.get();
+ }
+ return copy;
+ }
}
diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle
index ffecdcd16f..2b5a6010a9 100644
--- a/extensions/ffmpeg/build.gradle
+++ b/extensions/ffmpeg/build.gradle
@@ -38,9 +38,10 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'androidx.annotation:annotation:1.0.2'
+ implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
index c5d80aa32b..39d1ee4094 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java
@@ -92,8 +92,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
- protected int supportsFormatInternal(DrmSessionManager drmSessionManager,
- Format format) {
+ protected int supportsFormatInternal(
+ @Nullable DrmSessionManager drmSessionManager, Format format) {
Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE;
@@ -113,7 +113,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
- protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
+ protected FfmpegDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException {
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
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 7c5864420a..c78b02aa5b 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
@@ -42,7 +42,7 @@ import java.util.List;
private static final int DECODER_ERROR_OTHER = -2;
private final String codecName;
- private final @Nullable byte[] extraData;
+ @Nullable private final byte[] extraData;
private final @C.Encoding int encoding;
private final int outputBufferSize;
@@ -172,28 +172,49 @@ import java.util.List;
private static @Nullable byte[] getExtraData(String mimeType, List initializationData) {
switch (mimeType) {
case MimeTypes.AUDIO_AAC:
- case MimeTypes.AUDIO_ALAC:
case MimeTypes.AUDIO_OPUS:
return initializationData.get(0);
+ case MimeTypes.AUDIO_ALAC:
+ return getAlacExtraData(initializationData);
case MimeTypes.AUDIO_VORBIS:
- byte[] header0 = initializationData.get(0);
- byte[] header1 = initializationData.get(1);
- byte[] extraData = new byte[header0.length + header1.length + 6];
- extraData[0] = (byte) (header0.length >> 8);
- extraData[1] = (byte) (header0.length & 0xFF);
- System.arraycopy(header0, 0, extraData, 2, header0.length);
- extraData[header0.length + 2] = 0;
- extraData[header0.length + 3] = 0;
- extraData[header0.length + 4] = (byte) (header1.length >> 8);
- extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
- System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
- return extraData;
+ return getVorbisExtraData(initializationData);
default:
// Other codecs do not require extra data.
return null;
}
}
+ private static byte[] getAlacExtraData(List initializationData) {
+ // FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC "magic cookie", as extra
+ // data. initializationData[0] contains only the magic cookie, and so we need to package it into
+ // an ALAC atom. See:
+ // https://ffmpeg.org/doxygen/0.6/alac_8c.html
+ // https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt
+ byte[] magicCookie = initializationData.get(0);
+ int alacAtomLength = 12 + magicCookie.length;
+ ByteBuffer alacAtom = ByteBuffer.allocate(alacAtomLength);
+ alacAtom.putInt(alacAtomLength);
+ alacAtom.putInt(0x616c6163); // type=alac
+ alacAtom.putInt(0); // version=0, flags=0
+ alacAtom.put(magicCookie, /* offset= */ 0, magicCookie.length);
+ return alacAtom.array();
+ }
+
+ private static byte[] getVorbisExtraData(List initializationData) {
+ byte[] header0 = initializationData.get(0);
+ byte[] header1 = initializationData.get(1);
+ byte[] extraData = new byte[header0.length + header1.length + 6];
+ extraData[0] = (byte) (header0.length >> 8);
+ extraData[1] = (byte) (header0.length & 0xFF);
+ System.arraycopy(header0, 0, extraData, 2, header0.length);
+ extraData[header0.length + 2] = 0;
+ extraData[header0.length + 3] = 0;
+ extraData[header0.length + 4] = (byte) (header1.length >> 8);
+ extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
+ System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
+ return extraData;
+ }
+
private native long ffmpegInitialize(
String codecName,
@Nullable byte[] extraData,
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java
new file mode 100644
index 0000000000..a9fedb19cb
--- /dev/null
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.ffmpeg;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/ffmpeg/src/test/AndroidManifest.xml b/extensions/ffmpeg/src/test/AndroidManifest.xml
index d53bca4ca2..6ec1cea289 100644
--- a/extensions/ffmpeg/src/test/AndroidManifest.xml
+++ b/extensions/ffmpeg/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle
index 06a5888404..dfac2e1c26 100644
--- a/extensions/flac/build.gradle
+++ b/extensions/flac/build.gradle
@@ -39,10 +39,12 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'androidx.annotation:annotation:1.0.2'
+ implementation 'androidx.annotation:annotation:1.1.0'
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt
index ee0a9fa5b5..3e52f643e7 100644
--- a/extensions/flac/proguard-rules.txt
+++ b/extensions/flac/proguard-rules.txt
@@ -9,6 +9,9 @@
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
*;
}
--keep class com.google.android.exoplayer2.util.FlacStreamInfo {
+-keep class com.google.android.exoplayer2.util.FlacStreamMetadata {
+ *;
+}
+-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame {
*;
}
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
index 435279fc45..a3770afc78 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
@@ -52,7 +52,10 @@ public final class FlacBinarySearchSeekerTest {
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
- decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
+ decoderJni.decodeStreamMetadata(),
+ /* firstFramePosition= */ 0,
+ data.length,
+ decoderJni);
SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull();
@@ -70,7 +73,10 @@ public final class FlacBinarySearchSeekerTest {
decoderJni.setData(input);
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
- decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
+ decoderJni.decodeStreamMetadata(),
+ /* firstFramePosition= */ 0,
+ data.length,
+ decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.isSeeking()).isTrue();
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java
index 6008d99448..3beb4d0103 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java
@@ -228,7 +228,8 @@ public final class FlacExtractorSeekTest {
}
}
- private @Nullable SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
+ @Nullable
+ private SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
throws IOException, InterruptedException {
try {
ExtractorInput input = getExtractorInputFromPosition(0);
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
index d9cbac6ad5..97f152cea4 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
@@ -28,7 +28,7 @@ import org.junit.runner.RunWith;
public class FlacExtractorTest {
@Before
- public void setUp() throws Exception {
+ public void setUp() {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
index 12ef68ee3c..c10d6fdb27 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java
@@ -82,7 +82,7 @@ public class FlacPlaybackTest {
public void run() {
Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
- DefaultTrackSelector trackSelector = new DefaultTrackSelector();
+ DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
MediaSource mediaSource =
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
index b9c6ea06dd..4bfcc003ec 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
@@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -34,20 +34,20 @@ import java.nio.ByteBuffer;
private final FlacDecoderJni decoderJni;
public FlacBinarySearchSeeker(
- FlacStreamInfo streamInfo,
+ FlacStreamMetadata streamMetadata,
long firstFramePosition,
long inputLength,
FlacDecoderJni decoderJni) {
super(
- new FlacSeekTimestampConverter(streamInfo),
+ new FlacSeekTimestampConverter(streamMetadata),
new FlacTimestampSeeker(decoderJni),
- streamInfo.durationUs(),
+ streamMetadata.durationUs(),
/* floorTimePosition= */ 0,
- /* ceilingTimePosition= */ streamInfo.totalSamples,
+ /* ceilingTimePosition= */ streamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength,
- /* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(),
- /* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize));
+ /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
+ /* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni);
}
@@ -112,15 +112,15 @@ import java.nio.ByteBuffer;
* the timestamp for a stream seek time position.
*/
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
- private final FlacStreamInfo streamInfo;
+ private final FlacStreamMetadata streamMetadata;
- public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
- this.streamInfo = streamInfo;
+ public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {
+ this.streamMetadata = streamMetadata;
}
@Override
public long timeUsToTargetTime(long timeUs) {
- return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
+ return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);
}
}
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
index 2d74bce5f1..50eb048d98 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
@@ -15,11 +15,13 @@
*/
package com.google.android.exoplayer2.ext.flac;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
@@ -56,21 +58,20 @@ import java.util.List;
}
decoderJni = new FlacDecoderJni();
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
- FlacStreamInfo streamInfo;
+ FlacStreamMetadata streamMetadata;
try {
- streamInfo = decoderJni.decodeMetadata();
+ streamMetadata = decoderJni.decodeStreamMetadata();
+ } catch (ParserException e) {
+ throw new FlacDecoderException("Failed to decode StreamInfo", e);
} catch (IOException | InterruptedException e) {
// Never happens.
throw new IllegalStateException(e);
}
- if (streamInfo == null) {
- throw new FlacDecoderException("Metadata decoding failed");
- }
int initialInputBufferSize =
- maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;
+ maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize);
- maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
+ maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
}
@Override
@@ -94,6 +95,7 @@ import java.util.List;
}
@Override
+ @Nullable
protected FlacDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
index de038921aa..f454e28c68 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
@@ -15,9 +15,12 @@
*/
package com.google.android.exoplayer2.ext.flac;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -37,14 +40,14 @@ import java.nio.ByteBuffer;
}
}
- private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
+ private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac.
private final long nativeDecoderContext;
- private ByteBuffer byteBufferData;
- private ExtractorInput extractorInput;
+ @Nullable private ByteBuffer byteBufferData;
+ @Nullable private ExtractorInput extractorInput;
+ @Nullable private byte[] tempBuffer;
private boolean endOfExtractorInput;
- private byte[] tempBuffer;
public FlacDecoderJni() throws FlacDecoderException {
if (!FlacLibrary.isAvailable()) {
@@ -57,67 +60,79 @@ import java.nio.ByteBuffer;
}
/**
- * Sets data to be parsed by libflac.
- * @param byteBufferData Source {@link ByteBuffer}
+ * Sets the data to be parsed.
+ *
+ * @param byteBufferData Source {@link ByteBuffer}.
*/
public void setData(ByteBuffer byteBufferData) {
this.byteBufferData = byteBufferData;
this.extractorInput = null;
- this.tempBuffer = null;
}
/**
- * Sets data to be parsed by libflac.
- * @param extractorInput Source {@link ExtractorInput}
+ * Sets the data to be parsed.
+ *
+ * @param extractorInput Source {@link ExtractorInput}.
*/
public void setData(ExtractorInput extractorInput) {
this.byteBufferData = null;
this.extractorInput = extractorInput;
- if (tempBuffer == null) {
- this.tempBuffer = new byte[TEMP_BUFFER_SIZE];
- }
endOfExtractorInput = false;
+ if (tempBuffer == null) {
+ tempBuffer = new byte[TEMP_BUFFER_SIZE];
+ }
}
+ /**
+ * Returns whether the end of the data to be parsed has been reached, or true if no data was set.
+ */
public boolean isEndOfData() {
if (byteBufferData != null) {
return byteBufferData.remaining() == 0;
} else if (extractorInput != null) {
return endOfExtractorInput;
+ } else {
+ return true;
}
- return true;
+ }
+
+ /** Clears the data to be parsed. */
+ public void clearData() {
+ byteBufferData = null;
+ extractorInput = null;
}
/**
* Reads up to {@code length} bytes from the data source.
- *
- * This method blocks until at least one byte of data can be read, the end of the input is
+ *
+ *
This method blocks until at least one byte of data can be read, the end of the input is
* detected or an exception is thrown.
- *
- * This method is called from the native code.
*
* @param target A target {@link ByteBuffer} into which data should be written.
- * @return Returns the number of bytes read, or -1 on failure. It's not an error if this returns
- * zero; it just means all the data read from the source.
+ * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been
+ * read from the source, then 0 is returned.
*/
+ @SuppressWarnings("unused") // Called from native code.
public int read(ByteBuffer target) throws IOException, InterruptedException {
int byteCount = target.remaining();
if (byteBufferData != null) {
byteCount = Math.min(byteCount, byteBufferData.remaining());
int originalLimit = byteBufferData.limit();
byteBufferData.limit(byteBufferData.position() + byteCount);
-
target.put(byteBufferData);
-
byteBufferData.limit(originalLimit);
} else if (extractorInput != null) {
+ ExtractorInput extractorInput = this.extractorInput;
+ byte[] tempBuffer = Util.castNonNull(this.tempBuffer);
byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE);
- int read = readFromExtractorInput(0, byteCount);
+ int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount);
if (read < 4) {
// Reading less than 4 bytes, most of the time, happens because of getting the bytes left in
// the buffer of the input. Do another read to reduce the number of calls to this method
// from the native code.
- read += readFromExtractorInput(read, byteCount - read);
+ read +=
+ readFromExtractorInput(
+ extractorInput, tempBuffer, read, /* length= */ byteCount - read);
}
byteCount = read;
target.put(tempBuffer, 0, byteCount);
@@ -127,9 +142,13 @@ import java.nio.ByteBuffer;
return byteCount;
}
- /** Decodes and consumes the StreamInfo section from the FLAC stream. */
- public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException {
- return flacDecodeMetadata(nativeDecoderContext);
+ /** Decodes and consumes the metadata from the FLAC stream. */
+ public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {
+ FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);
+ if (streamMetadata == null) {
+ throw new ParserException("Failed to decode stream metadata");
+ }
+ return streamMetadata;
}
/**
@@ -234,7 +253,8 @@ import java.nio.ByteBuffer;
flacRelease(nativeDecoderContext);
}
- private int readFromExtractorInput(int offset, int length)
+ private int readFromExtractorInput(
+ ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length)
throws IOException, InterruptedException {
int read = extractorInput.read(tempBuffer, offset, length);
if (read == C.RESULT_END_OF_INPUT) {
@@ -246,7 +266,7 @@ import java.nio.ByteBuffer;
private native long flacInit();
- private native FlacStreamInfo flacDecodeMetadata(long context)
+ private native FlacStreamMetadata flacDecodeMetadata(long context)
throws IOException, InterruptedException;
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
index bb72e114fe..cd91b06288 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
@@ -21,7 +21,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
+import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@@ -33,7 +33,8 @@ import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
@@ -42,6 +43,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.Arrays;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Facilitates the extraction of data from the FLAC container format.
@@ -74,23 +78,20 @@ public final class FlacExtractor implements Extractor {
*/
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
+ private final ParsableByteArray outputBuffer;
private final Id3Peeker id3Peeker;
- private final boolean isId3MetadataDisabled;
+ private final boolean id3MetadataDisabled;
- private FlacDecoderJni decoderJni;
+ @Nullable private FlacDecoderJni decoderJni;
+ private @MonotonicNonNull ExtractorOutput extractorOutput;
+ private @MonotonicNonNull TrackOutput trackOutput;
- private ExtractorOutput extractorOutput;
- private TrackOutput trackOutput;
+ private boolean streamMetadataDecoded;
+ private @MonotonicNonNull FlacStreamMetadata streamMetadata;
+ private @MonotonicNonNull OutputFrameHolder outputFrameHolder;
- private ParsableByteArray outputBuffer;
- private ByteBuffer outputByteBuffer;
- private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
- private FlacStreamInfo streamInfo;
-
- private Metadata id3Metadata;
- private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
-
- private boolean readPastStreamInfo;
+ @Nullable private Metadata id3Metadata;
+ @Nullable private FlacBinarySearchSeeker binarySearchSeeker;
/** Constructs an instance with flags = 0. */
public FlacExtractor() {
@@ -103,8 +104,9 @@ public final class FlacExtractor implements Extractor {
* @param flags Flags that control the extractor's behavior.
*/
public FlacExtractor(int flags) {
+ outputBuffer = new ParsableByteArray();
id3Peeker = new Id3Peeker();
- isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
+ id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
}
@Override
@@ -130,48 +132,53 @@ public final class FlacExtractor implements Extractor {
@Override
public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
- if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
+ if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input);
}
- decoderJni.setData(input);
- readPastStreamInfo(input);
-
- if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
- return handlePendingSeek(input, seekPosition);
- }
-
- long lastDecodePosition = decoderJni.getDecodePosition();
+ FlacDecoderJni decoderJni = initDecoderJni(input);
try {
- decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
- } catch (FlacDecoderJni.FlacFrameDecodeException e) {
- throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
- }
- int outputSize = outputByteBuffer.limit();
- if (outputSize == 0) {
- return RESULT_END_OF_INPUT;
- }
+ decodeStreamMetadata(input);
- writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp());
- return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
+ if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
+ return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);
+ }
+
+ ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
+ long lastDecodePosition = decoderJni.getDecodePosition();
+ try {
+ decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
+ } catch (FlacDecoderJni.FlacFrameDecodeException e) {
+ throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
+ }
+ int outputSize = outputByteBuffer.limit();
+ if (outputSize == 0) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput);
+ return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
+ } finally {
+ decoderJni.clearData();
+ }
}
@Override
public void seek(long position, long timeUs) {
if (position == 0) {
- readPastStreamInfo = false;
+ streamMetadataDecoded = false;
}
if (decoderJni != null) {
decoderJni.reset(position);
}
- if (flacBinarySearchSeeker != null) {
- flacBinarySearchSeeker.setSeekTargetUs(timeUs);
+ if (binarySearchSeeker != null) {
+ binarySearchSeeker.setSeekTargetUs(timeUs);
}
}
@Override
public void release() {
- flacBinarySearchSeeker = null;
+ binarySearchSeeker = null;
if (decoderJni != null) {
decoderJni.release();
decoderJni = null;
@@ -179,123 +186,141 @@ public final class FlacExtractor implements Extractor {
}
/**
- * Peeks ID3 tag data (if present) at the beginning of the input.
+ * Peeks ID3 tag data at the beginning of the input.
*
- * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
- * present in the input.
+ * @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input.
*/
@Nullable
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition();
Id3Decoder.FramePredicate id3FramePredicate =
- isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
+ id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate);
}
+ @EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized.
+ @SuppressWarnings({"contracts.postcondition.not.satisfied"})
+ private FlacDecoderJni initDecoderJni(ExtractorInput input) {
+ FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni);
+ decoderJni.setData(input);
+ return decoderJni;
+ }
+
+ @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
+ @EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
+ @SuppressWarnings({"contracts.postcondition.not.satisfied"})
+ private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {
+ if (streamMetadataDecoded) {
+ return;
+ }
+
+ FlacStreamMetadata streamMetadata;
+ try {
+ streamMetadata = decoderJni.decodeStreamMetadata();
+ } catch (IOException e) {
+ decoderJni.reset(/* newPosition= */ 0);
+ input.setRetryPosition(/* position= */ 0, e);
+ throw e;
+ }
+
+ streamMetadataDecoded = true;
+ if (this.streamMetadata == null) {
+ this.streamMetadata = streamMetadata;
+ binarySearchSeeker =
+ outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);
+ Metadata metadata = id3MetadataDisabled ? null : id3Metadata;
+ if (streamMetadata.metadata != null) {
+ metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata);
+ }
+ outputFormat(streamMetadata, metadata, trackOutput);
+ outputBuffer.reset(streamMetadata.maxDecodedFrameSize());
+ outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
+ }
+ }
+
+ @RequiresNonNull("binarySearchSeeker")
+ private int handlePendingSeek(
+ ExtractorInput input,
+ PositionHolder seekPosition,
+ ParsableByteArray outputBuffer,
+ OutputFrameHolder outputFrameHolder,
+ TrackOutput trackOutput)
+ throws InterruptedException, IOException {
+ int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
+ ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
+ if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
+ outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput);
+ }
+ return seekResult;
+ }
+
/**
* Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.
*
* @return Whether the input begins with {@link #FLAC_SIGNATURE}.
*/
- private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException {
+ private static boolean peekFlacSignature(ExtractorInput input)
+ throws IOException, InterruptedException {
byte[] header = new byte[FLAC_SIGNATURE.length];
- input.peekFully(header, 0, FLAC_SIGNATURE.length);
+ input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length);
return Arrays.equals(header, FLAC_SIGNATURE);
}
- private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
- if (readPastStreamInfo) {
- return;
- }
-
- FlacStreamInfo streamInfo = decodeStreamInfo(input);
- readPastStreamInfo = true;
- if (this.streamInfo == null) {
- updateFlacStreamInfo(input, streamInfo);
- }
- }
-
- private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) {
- this.streamInfo = streamInfo;
- outputSeekMap(input, streamInfo);
- outputFormat(streamInfo);
- outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
- outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
- outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
- }
-
- private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
- throws InterruptedException, IOException {
- try {
- FlacStreamInfo streamInfo = decoderJni.decodeMetadata();
- if (streamInfo == null) {
- throw new IOException("Metadata decoding failed");
- }
- return streamInfo;
- } catch (IOException e) {
- decoderJni.reset(0);
- input.setRetryPosition(0, e);
- throw e;
- }
- }
-
- private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) {
- boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1;
- SeekMap seekMap =
- hasSeekTable
- ? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
- : getSeekMapForNonSeekTableFlac(input, streamInfo);
- extractorOutput.seekMap(seekMap);
- }
-
- private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) {
- long inputLength = input.getLength();
- if (inputLength != C.LENGTH_UNSET) {
+ /**
+ * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
+ * handle seeks.
+ */
+ @Nullable
+ private static FlacBinarySearchSeeker outputSeekMap(
+ FlacDecoderJni decoderJni,
+ FlacStreamMetadata streamMetadata,
+ long streamLength,
+ ExtractorOutput output) {
+ boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1;
+ FlacBinarySearchSeeker binarySearchSeeker = null;
+ SeekMap seekMap;
+ if (hasSeekTable) {
+ seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
+ } else if (streamLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition();
- flacBinarySearchSeeker =
- new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
- return flacBinarySearchSeeker.getSeekMap();
- } else { // can't seek at all, because there's no SeekTable and the input length is unknown.
- return new SeekMap.Unseekable(streamInfo.durationUs());
+ binarySearchSeeker =
+ new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);
+ seekMap = binarySearchSeeker.getSeekMap();
+ } else {
+ seekMap = new SeekMap.Unseekable(streamMetadata.durationUs());
}
+ output.seekMap(seekMap);
+ return binarySearchSeeker;
}
- private void outputFormat(FlacStreamInfo streamInfo) {
+ private static void outputFormat(
+ FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
Format mediaFormat =
Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
- streamInfo.bitRate(),
- streamInfo.maxDecodedFrameSize(),
- streamInfo.channels,
- streamInfo.sampleRate,
- getPcmEncoding(streamInfo.bitsPerSample),
+ streamMetadata.bitRate(),
+ streamMetadata.maxDecodedFrameSize(),
+ streamMetadata.channels,
+ streamMetadata.sampleRate,
+ getPcmEncoding(streamMetadata.bitsPerSample),
/* encoderDelay= */ 0,
/* encoderPadding= */ 0,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null,
- isId3MetadataDisabled ? null : id3Metadata);
- trackOutput.format(mediaFormat);
+ metadata);
+ output.format(mediaFormat);
}
- private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
- throws InterruptedException, IOException {
- int seekResult =
- flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
- ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
- if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
- writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
- }
- return seekResult;
- }
-
- private void writeLastSampleToOutput(int size, long lastSampleTimestamp) {
- outputBuffer.setPosition(0);
- trackOutput.sampleData(outputBuffer, size);
- trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null);
+ private static void outputSample(
+ ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) {
+ sampleData.setPosition(0);
+ output.sampleData(sampleData, size);
+ output.sampleMetadata(
+ timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);
}
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
index ac7646cc4b..d833c47d14 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.flac;
import android.os.Handler;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
@@ -33,7 +34,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
private static final int NUM_BUFFERS = 16;
public LibflacAudioRenderer() {
- this(null, null);
+ this(/* eventHandler= */ null, /* eventListener= */ null);
}
/**
@@ -43,15 +44,15 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public LibflacAudioRenderer(
- Handler eventHandler,
- AudioRendererEventListener eventListener,
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
@Override
- protected int supportsFormatInternal(DrmSessionManager drmSessionManager,
- Format format) {
+ protected int supportsFormatInternal(
+ @Nullable DrmSessionManager drmSessionManager, Format format) {
if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
@@ -65,7 +66,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
- protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
+ protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FlacDecoderException {
return new FlacDecoder(
NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java
new file mode 100644
index 0000000000..ef6da7e3c6
--- /dev/null
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.flac;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc
index 298719d48d..d60a7cead2 100644
--- a/extensions/flac/src/main/jni/flac_jni.cc
+++ b/extensions/flac/src/main/jni/flac_jni.cc
@@ -14,9 +14,12 @@
* limitations under the License.
*/
-#include
#include
+#include
+
#include
+#include
+
#include "include/flac_parser.h"
#define LOG_TAG "flac_jni"
@@ -95,19 +98,68 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
return NULL;
}
+ jclass arrayListClass = env->FindClass("java/util/ArrayList");
+ jmethodID arrayListConstructor =
+ env->GetMethodID(arrayListClass, "", "()V");
+ jobject commentList = env->NewObject(arrayListClass, arrayListConstructor);
+ jmethodID arrayListAddMethod =
+ env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
+
+ if (context->parser->areVorbisCommentsValid()) {
+ std::vector vorbisComments =
+ context->parser->getVorbisComments();
+ for (std::vector::const_iterator vorbisComment =
+ vorbisComments.begin();
+ vorbisComment != vorbisComments.end(); ++vorbisComment) {
+ jstring commentString = env->NewStringUTF((*vorbisComment).c_str());
+ env->CallBooleanMethod(commentList, arrayListAddMethod, commentString);
+ env->DeleteLocalRef(commentString);
+ }
+ }
+
+ jobject pictureFrames = env->NewObject(arrayListClass, arrayListConstructor);
+ bool picturesValid = context->parser->arePicturesValid();
+ if (picturesValid) {
+ std::vector pictures = context->parser->getPictures();
+ jclass pictureFrameClass = env->FindClass(
+ "com/google/android/exoplayer2/metadata/flac/PictureFrame");
+ jmethodID pictureFrameConstructor =
+ env->GetMethodID(pictureFrameClass, "",
+ "(ILjava/lang/String;Ljava/lang/String;IIII[B)V");
+ for (std::vector::const_iterator picture = pictures.begin();
+ picture != pictures.end(); ++picture) {
+ jstring mimeType = env->NewStringUTF(picture->mimeType.c_str());
+ jstring description = env->NewStringUTF(picture->description.c_str());
+ jbyteArray pictureData = env->NewByteArray(picture->data.size());
+ env->SetByteArrayRegion(pictureData, 0, picture->data.size(),
+ (signed char *)&picture->data[0]);
+ jobject pictureFrame = env->NewObject(
+ pictureFrameClass, pictureFrameConstructor, picture->type, mimeType,
+ description, picture->width, picture->height, picture->depth,
+ picture->colors, pictureData);
+ env->CallBooleanMethod(pictureFrames, arrayListAddMethod, pictureFrame);
+ env->DeleteLocalRef(mimeType);
+ env->DeleteLocalRef(description);
+ env->DeleteLocalRef(pictureData);
+ }
+ }
+
const FLAC__StreamMetadata_StreamInfo &streamInfo =
context->parser->getStreamInfo();
- jclass cls = env->FindClass(
+ jclass flacStreamMetadataClass = env->FindClass(
"com/google/android/exoplayer2/util/"
- "FlacStreamInfo");
- jmethodID constructor = env->GetMethodID(cls, "", "(IIIIIIIJ)V");
+ "FlacStreamMetadata");
+ jmethodID flacStreamMetadataConstructor =
+ env->GetMethodID(flacStreamMetadataClass, "",
+ "(IIIIIIIJLjava/util/List;Ljava/util/List;)V");
- return env->NewObject(cls, constructor, streamInfo.min_blocksize,
- streamInfo.max_blocksize, streamInfo.min_framesize,
- streamInfo.max_framesize, streamInfo.sample_rate,
- streamInfo.channels, streamInfo.bits_per_sample,
- streamInfo.total_samples);
+ return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
+ streamInfo.min_blocksize, streamInfo.max_blocksize,
+ streamInfo.min_framesize, streamInfo.max_framesize,
+ streamInfo.sample_rate, streamInfo.channels,
+ streamInfo.bits_per_sample, streamInfo.total_samples,
+ commentList, pictureFrames);
}
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {
diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc
index 83d3367415..830f3e2178 100644
--- a/extensions/flac/src/main/jni/flac_parser.cc
+++ b/extensions/flac/src/main/jni/flac_parser.cc
@@ -172,6 +172,43 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {
case FLAC__METADATA_TYPE_SEEKTABLE:
mSeekTable = &metadata->data.seek_table;
break;
+ case FLAC__METADATA_TYPE_VORBIS_COMMENT:
+ if (!mVorbisCommentsValid) {
+ FLAC__StreamMetadata_VorbisComment vorbisComment =
+ metadata->data.vorbis_comment;
+ for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) {
+ FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry =
+ vorbisComment.comments[i];
+ if (vorbisCommentEntry.entry != NULL) {
+ std::string comment(
+ reinterpret_cast(vorbisCommentEntry.entry),
+ vorbisCommentEntry.length);
+ mVorbisComments.push_back(comment);
+ }
+ }
+ mVorbisCommentsValid = true;
+ } else {
+ ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT");
+ }
+ break;
+ case FLAC__METADATA_TYPE_PICTURE: {
+ const FLAC__StreamMetadata_Picture *parsedPicture =
+ &metadata->data.picture;
+ FlacPicture picture;
+ picture.mimeType.assign(std::string(parsedPicture->mime_type));
+ picture.description.assign(
+ std::string((char *)parsedPicture->description));
+ picture.data.assign(parsedPicture->data,
+ parsedPicture->data + parsedPicture->data_length);
+ picture.width = parsedPicture->width;
+ picture.height = parsedPicture->height;
+ picture.depth = parsedPicture->depth;
+ picture.colors = parsedPicture->colors;
+ picture.type = parsedPicture->type;
+ mPictures.push_back(picture);
+ mPicturesValid = true;
+ break;
+ }
default:
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
break;
@@ -233,6 +270,8 @@ FLACParser::FLACParser(DataSource *source)
mCurrentPos(0LL),
mEOF(false),
mStreamInfoValid(false),
+ mVorbisCommentsValid(false),
+ mPicturesValid(false),
mWriteRequested(false),
mWriteCompleted(false),
mWriteBuffer(NULL),
@@ -266,6 +305,10 @@ bool FLACParser::init() {
FLAC__METADATA_TYPE_STREAMINFO);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_SEEKTABLE);
+ FLAC__stream_decoder_set_metadata_respond(mDecoder,
+ FLAC__METADATA_TYPE_VORBIS_COMMENT);
+ FLAC__stream_decoder_set_metadata_respond(mDecoder,
+ FLAC__METADATA_TYPE_PICTURE);
FLAC__StreamDecoderInitStatus initStatus;
initStatus = FLAC__stream_decoder_init_stream(
mDecoder, read_callback, seek_callback, tell_callback, length_callback,
diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h
index cea7fbe33b..fd3e36a806 100644
--- a/extensions/flac/src/main/jni/include/flac_parser.h
+++ b/extensions/flac/src/main/jni/include/flac_parser.h
@@ -19,6 +19,10 @@
#include
+#include
+#include
+#include
+
// libFLAC parser
#include "FLAC/stream_decoder.h"
@@ -26,6 +30,17 @@
typedef int status_t;
+struct FlacPicture {
+ int type;
+ std::string mimeType;
+ std::string description;
+ FLAC__uint32 width;
+ FLAC__uint32 height;
+ FLAC__uint32 depth;
+ FLAC__uint32 colors;
+ std::vector data;
+};
+
class FLACParser {
public:
FLACParser(DataSource *source);
@@ -44,6 +59,16 @@ class FLACParser {
return mStreamInfo;
}
+ bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }
+
+ const std::vector& getVorbisComments() const {
+ return mVorbisComments;
+ }
+
+ bool arePicturesValid() const { return mPicturesValid; }
+
+ const std::vector &getPictures() const { return mPictures; }
+
int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
}
@@ -71,6 +96,10 @@ class FLACParser {
mEOF = false;
if (newPosition == 0) {
mStreamInfoValid = false;
+ mVorbisCommentsValid = false;
+ mPicturesValid = false;
+ mVorbisComments.clear();
+ mPictures.clear();
FLAC__stream_decoder_reset(mDecoder);
} else {
FLAC__stream_decoder_flush(mDecoder);
@@ -116,6 +145,14 @@ class FLACParser {
const FLAC__StreamMetadata_SeekTable *mSeekTable;
uint64_t firstFrameOffset;
+ // cached when the VORBIS_COMMENT metadata is parsed by libFLAC
+ std::vector mVorbisComments;
+ bool mVorbisCommentsValid;
+
+ // cached when the PICTURE metadata is parsed by libFLAC
+ std::vector mPictures;
+ bool mPicturesValid;
+
// cached when a decoded PCM block is "written" by libFLAC parser
bool mWriteRequested;
bool mWriteCompleted;
diff --git a/extensions/flac/src/test/AndroidManifest.xml b/extensions/flac/src/test/AndroidManifest.xml
index 1d68b376ac..509151aa21 100644
--- a/extensions/flac/src/test/AndroidManifest.xml
+++ b/extensions/flac/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle
index 50acd6c040..1031d6f4b7 100644
--- a/extensions/gvr/build.gradle
+++ b/extensions/gvr/build.gradle
@@ -33,7 +33,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
- implementation 'androidx.annotation:annotation:1.0.2'
+ implementation 'androidx.annotation:annotation:1.1.0'
api 'com.google.vr:sdk-base:1.190.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}
diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java
index 2c912c17f2..e22c97859a 100644
--- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java
+++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrPlayerActivity.java
@@ -50,7 +50,10 @@ import com.google.vr.sdk.controller.ControllerManager;
import javax.microedition.khronos.egl.EGLConfig;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
-/** Base activity for VR 360 video playback. */
+/**
+ * Base activity for VR 360 video playback. Before starting the video playback a player needs to be
+ * set using {@link #setPlayer(Player)}.
+ */
public abstract class GvrPlayerActivity extends GvrActivity {
private static final int EXIT_FROM_VR_REQUEST_CODE = 42;
@@ -58,12 +61,12 @@ public abstract class GvrPlayerActivity extends GvrActivity {
private final Handler mainHandler;
@Nullable private Player player;
- @MonotonicNonNull private GlViewGroup glView;
- @MonotonicNonNull private ControllerManager controllerManager;
- @MonotonicNonNull private SurfaceTexture surfaceTexture;
- @MonotonicNonNull private Surface surface;
- @MonotonicNonNull private SceneRenderer scene;
- @MonotonicNonNull private PlayerControlView playerControl;
+ private @MonotonicNonNull GlViewGroup glView;
+ private @MonotonicNonNull ControllerManager controllerManager;
+ private @MonotonicNonNull SurfaceTexture surfaceTexture;
+ private @MonotonicNonNull Surface surface;
+ private @MonotonicNonNull SceneRenderer scene;
+ private @MonotonicNonNull PlayerControlView playerControl;
public GvrPlayerActivity() {
mainHandler = new Handler(Looper.getMainLooper());
diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle
index a91bbbd981..340e9832be 100644
--- a/extensions/ima/build.gradle
+++ b/extensions/ima/build.gradle
@@ -32,10 +32,12 @@ android {
}
dependencies {
- api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
+ api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3'
implementation project(modulePrefix + 'library-core')
+ implementation 'androidx.annotation:annotation:1.1.0'
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
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 bdeebec44c..e37f192c97 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
@@ -313,14 +313,14 @@ public final class ImaAdsLoader
*/
private static final int IMA_AD_STATE_PAUSED = 2;
- private final @Nullable Uri adTagUri;
- private final @Nullable String adsResponse;
+ @Nullable private final Uri adTagUri;
+ @Nullable private final String adsResponse;
private final int vastLoadTimeoutMs;
private final int mediaLoadTimeoutMs;
private final boolean focusSkipButtonWhenAvailable;
private final int mediaBitrate;
- private final @Nullable Set adUiElements;
- private final @Nullable AdEventListener adEventListener;
+ @Nullable private final Set adUiElements;
+ @Nullable private final AdEventListener adEventListener;
private final ImaFactory imaFactory;
private final Timeline.Period period;
private final List adCallbacks;
@@ -426,7 +426,7 @@ public final class ImaAdsLoader
* @deprecated Use {@link ImaAdsLoader.Builder}.
*/
@Deprecated
- public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) {
+ public ImaAdsLoader(Context context, Uri adTagUri, @Nullable ImaSdkSettings imaSdkSettings) {
this(
context,
adTagUri,
@@ -946,8 +946,7 @@ public final class ImaAdsLoader
// Player.EventListener implementation.
@Override
- public void onTimelineChanged(
- Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
+ public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
if (timeline.isEmpty()) {
// The player is being reset or contains no media.
return;
@@ -1054,13 +1053,8 @@ public final class ImaAdsLoader
long contentPositionMs = player.getCurrentPosition();
int adGroupIndexForPosition =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
- if (adGroupIndexForPosition == 0) {
- podIndexOffset = 0;
- } else if (adGroupIndexForPosition == C.INDEX_UNSET) {
- // There is no preroll and midroll pod indices start at 1.
- podIndexOffset = -1;
- } else /* adGroupIndexForPosition > 0 */ {
- // Skip ad groups before the one at or immediately before the playback position.
+ if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) {
+ // Skip any ad groups before the one at or immediately before the playback position.
for (int i = 0; i < adGroupIndexForPosition; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
@@ -1070,9 +1064,18 @@ public final class ImaAdsLoader
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
+ }
- // We're removing one or more ads, which means that the earliest ad (if any) will be a
- // midroll/postroll. Midroll pod indices start at 1.
+ // IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0.
+ // Store an index offset as we want to index all ads (including skipped ones) from 0.
+ if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) {
+ // We are playing a preroll.
+ podIndexOffset = 0;
+ } else if (adGroupIndexForPosition == C.INDEX_UNSET) {
+ // There's no ad to play which means there's no preroll.
+ podIndexOffset = -1;
+ } else {
+ // We are playing a midroll and any ads before it were skipped.
podIndexOffset = adGroupIndexForPosition - 1;
}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java
new file mode 100644
index 0000000000..9a382eb18f
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.ima;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/ima/src/test/AndroidManifest.xml b/extensions/ima/src/test/AndroidManifest.xml
index 9a4e33189e..564c5d94dd 100644
--- a/extensions/ima/src/test/AndroidManifest.xml
+++ b/extensions/ima/src/test/AndroidManifest.xml
@@ -13,4 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
index a9d6a37fac..a9572b7a8d 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
@@ -51,9 +51,7 @@ import java.util.ArrayList;
public void updateTimeline(Timeline timeline) {
for (Player.EventListener listener : listeners) {
listener.onTimelineChanged(
- timeline,
- null,
- prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
+ timeline, prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
}
prepared = true;
}
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 1e1935c63a..ab880703ee 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
@@ -252,7 +252,8 @@ public class ImaAdsLoaderTest {
}
@Override
- public @Nullable Ad getAd() {
+ @Nullable
+ public Ad getAd() {
return ad;
}
diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md
index f70125ba38..a6f0c3966a 100644
--- a/extensions/jobdispatcher/README.md
+++ b/extensions/jobdispatcher/README.md
@@ -1,7 +1,11 @@
# ExoPlayer Firebase JobDispatcher extension #
+**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.**
+
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
+[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
+[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ##
@@ -20,4 +24,3 @@ locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
-
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 d79dead0d7..c8975275f1 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
@@ -54,7 +54,10 @@ import com.google.android.exoplayer2.util.Util;
*
* @see GoogleApiAvailability
+ * @deprecated Use com.google.android.exoplayer2.ext.workmanager.WorkManagerScheduler or {@link
+ * com.google.android.exoplayer2.scheduler.PlatformScheduler}.
*/
+@Deprecated
public final class JobDispatcherScheduler implements Scheduler {
private static final boolean DEBUG = false;
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java
new file mode 100644
index 0000000000..a66904b505
--- /dev/null
+++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.jobdispatcher;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle
index c6f5a216ce..ecaa78e25b 100644
--- a/extensions/leanback/build.gradle
+++ b/extensions/leanback/build.gradle
@@ -32,7 +32,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'androidx.annotation:annotation:1.0.2'
+ implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.leanback:leanback:1.0.0'
}
diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
index 5705b73ab2..370e5515e8 100644
--- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
+++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
@@ -51,10 +51,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
private final ComponentListener componentListener;
private final int updatePeriodMs;
- private @Nullable PlaybackPreparer playbackPreparer;
+ @Nullable private PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher;
- private @Nullable ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
- private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost;
+ @Nullable private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
+ @Nullable private SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean hasSurface;
private boolean lastNotifiedPreparedState;
@@ -288,8 +288,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
}
@Override
- public void onTimelineChanged(
- Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
+ public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java
new file mode 100644
index 0000000000..79c544fc0f
--- /dev/null
+++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.leanback;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle
index 6c6ddf4ce4..7ee973723c 100644
--- a/extensions/mediasession/build.gradle
+++ b/extensions/mediasession/build.gradle
@@ -33,6 +33,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
api 'androidx.media:media:1.0.1'
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}
ext {
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 afe53099ee..cb1788f2fc 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
@@ -52,6 +52,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
/**
* Connects a {@link MediaSessionCompat} to a {@link Player}.
@@ -172,7 +173,7 @@ public final class MediaSessionConnector {
ResultReceiver cb);
}
- /** Interface to which playback preparation actions are delegated. */
+ /** Interface to which playback preparation and play actions are delegated. */
public interface PlaybackPreparer extends CommandReceiver {
long ACTIONS =
@@ -197,14 +198,36 @@ public final class MediaSessionConnector {
* @return The bitmask of the supported media actions.
*/
long getSupportedPrepareActions();
- /** See {@link MediaSessionCompat.Callback#onPrepare()}. */
- void onPrepare();
- /** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */
- void onPrepareFromMediaId(String mediaId, Bundle extras);
- /** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */
- void onPrepareFromSearch(String query, Bundle extras);
- /** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */
- void onPrepareFromUri(Uri uri, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepare()}.
+ *
+ * @param playWhenReady Whether playback should be started after preparation.
+ */
+ void onPrepare(boolean playWhenReady);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
+ *
+ * @param mediaId The media id of the media item to be prepared.
+ * @param playWhenReady Whether playback should be started after preparation.
+ * @param extras A {@link Bundle} of extras passed by the media controller.
+ */
+ void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
+ *
+ * @param query The search query.
+ * @param playWhenReady Whether playback should be started after preparation.
+ * @param extras A {@link Bundle} of extras passed by the media controller.
+ */
+ void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras);
+ /**
+ * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
+ *
+ * @param uri The {@link Uri} of the media item to be prepared.
+ * @param playWhenReady Whether playback should be started after preparation.
+ * @param extras A {@link Bundle} of extras passed by the media controller.
+ */
+ void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras);
}
/**
@@ -337,7 +360,7 @@ public final class MediaSessionConnector {
* @param extras Optional extras sent by a media controller.
*/
void onCustomAction(
- Player player, ControlDispatcher controlDispatcher, String action, Bundle extras);
+ Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras);
/**
* Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media
@@ -355,6 +378,13 @@ public final class MediaSessionConnector {
/**
* Gets the {@link MediaMetadataCompat} to be published to the session.
*
+ *
An app may need to load metadata resources like artwork bitmaps asynchronously. In such a
+ * case the app should return a {@link MediaMetadataCompat} object that does not contain these
+ * resources as a placeholder. The app should start an asynchronous operation to download the
+ * bitmap and put it into a cache. Finally, the app should call {@link
+ * #invalidateMediaSessionMetadata()}. This causes this callback to be called again and the app
+ * can now return a {@link MediaMetadataCompat} object with all the resources included.
+ *
* @param player The player connected to the media session.
* @return The {@link MediaMetadataCompat} to be published to the session.
*/
@@ -528,7 +558,7 @@ public final class MediaSessionConnector {
*
* @param queueNavigator The queue navigator.
*/
- public void setQueueNavigator(QueueNavigator queueNavigator) {
+ public void setQueueNavigator(@Nullable QueueNavigator queueNavigator) {
if (this.queueNavigator != queueNavigator) {
unregisterCommandReceiver(this.queueNavigator);
this.queueNavigator = queueNavigator;
@@ -541,7 +571,7 @@ public final class MediaSessionConnector {
*
* @param queueEditor The queue editor.
*/
- public void setQueueEditor(QueueEditor queueEditor) {
+ public void setQueueEditor(@Nullable QueueEditor queueEditor) {
if (this.queueEditor != queueEditor) {
unregisterCommandReceiver(this.queueEditor);
this.queueEditor = queueEditor;
@@ -643,7 +673,7 @@ public final class MediaSessionConnector {
mediaMetadataProvider != null && player != null
? mediaMetadataProvider.getMetadata(player)
: METADATA_EMPTY;
- mediaSession.setMetadata(metadata != null ? metadata : METADATA_EMPTY);
+ mediaSession.setMetadata(metadata);
}
/**
@@ -654,6 +684,7 @@ public final class MediaSessionConnector {
*/
public final void invalidateMediaSessionPlaybackState() {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+ @Nullable Player player = this.player;
if (player == null) {
builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
mediaSession.setPlaybackState(builder.build());
@@ -662,6 +693,7 @@ public final class MediaSessionConnector {
Map currentActions = new HashMap<>();
for (CustomActionProvider customActionProvider : customActionProviders) {
+ @Nullable
PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(player);
if (customAction != null) {
currentActions.put(customAction.getAction(), customActionProvider);
@@ -672,6 +704,7 @@ public final class MediaSessionConnector {
int playbackState = player.getPlaybackState();
Bundle extras = new Bundle();
+ @Nullable
ExoPlaybackException playbackError =
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
boolean reportError = playbackError != null || customError != null;
@@ -727,8 +760,8 @@ public final class MediaSessionConnector {
*
* @param commandReceiver The command receiver to register.
*/
- public void registerCustomCommandReceiver(CommandReceiver commandReceiver) {
- if (!customCommandReceivers.contains(commandReceiver)) {
+ public void registerCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null && !customCommandReceivers.contains(commandReceiver)) {
customCommandReceivers.add(commandReceiver);
}
}
@@ -738,18 +771,22 @@ public final class MediaSessionConnector {
*
* @param commandReceiver The command receiver to unregister.
*/
- public void unregisterCustomCommandReceiver(CommandReceiver commandReceiver) {
- customCommandReceivers.remove(commandReceiver);
+ public void unregisterCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null) {
+ customCommandReceivers.remove(commandReceiver);
+ }
}
- private void registerCommandReceiver(CommandReceiver commandReceiver) {
- if (!commandReceivers.contains(commandReceiver)) {
+ private void registerCommandReceiver(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null && !commandReceivers.contains(commandReceiver)) {
commandReceivers.add(commandReceiver);
}
}
- private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
- commandReceivers.remove(commandReceiver);
+ private void unregisterCommandReceiver(@Nullable CommandReceiver commandReceiver) {
+ if (commandReceiver != null) {
+ commandReceivers.remove(commandReceiver);
+ }
}
private long buildPrepareActions() {
@@ -807,39 +844,47 @@ public final class MediaSessionConnector {
}
}
+ @EnsuresNonNullIf(result = true, expression = "player")
private boolean canDispatchPlaybackAction(long action) {
return player != null && (enabledPlaybackActions & action) != 0;
}
+ @EnsuresNonNullIf(result = true, expression = "playbackPreparer")
private boolean canDispatchToPlaybackPreparer(long action) {
return playbackPreparer != null
&& (playbackPreparer.getSupportedPrepareActions() & action) != 0;
}
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "queueNavigator"})
private boolean canDispatchToQueueNavigator(long action) {
return player != null
&& queueNavigator != null
&& (queueNavigator.getSupportedQueueNavigatorActions(player) & action) != 0;
}
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "ratingCallback"})
private boolean canDispatchSetRating() {
return player != null && ratingCallback != null;
}
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "queueEditor"})
private boolean canDispatchQueueEdit() {
return player != null && queueEditor != null;
}
+ @EnsuresNonNullIf(
+ result = true,
+ expression = {"player", "mediaButtonEventHandler"})
private boolean canDispatchMediaButtonEvent() {
return player != null && mediaButtonEventHandler != null;
}
- private void setPlayWhenReady(boolean playWhenReady) {
- if (player != null) {
- controlDispatcher.dispatchSetPlayWhenReady(player, playWhenReady);
- }
- }
-
private void rewind(Player player) {
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
seekTo(player, player.getCurrentPosition() - rewindMs);
@@ -906,10 +951,10 @@ public final class MediaSessionConnector {
MediaSessionCompat.QueueItem queueItem = queue.get(i);
if (queueItem.getQueueId() == activeQueueItemId) {
MediaDescriptionCompat description = queueItem.getDescription();
- Bundle extras = description.getExtras();
+ @Nullable Bundle extras = description.getExtras();
if (extras != null) {
for (String key : extras.keySet()) {
- Object value = extras.get(key);
+ @Nullable Object value = extras.get(key);
if (value instanceof String) {
builder.putString(metadataExtrasPrefix + key, (String) value);
} else if (value instanceof CharSequence) {
@@ -925,38 +970,40 @@ public final class MediaSessionConnector {
}
}
}
- if (description.getTitle() != null) {
- String title = String.valueOf(description.getTitle());
- builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
- builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title);
+ @Nullable CharSequence title = description.getTitle();
+ if (title != null) {
+ String titleString = String.valueOf(title);
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, titleString);
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, titleString);
}
- if (description.getSubtitle() != null) {
+ @Nullable CharSequence subtitle = description.getSubtitle();
+ if (subtitle != null) {
builder.putString(
- MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
- String.valueOf(description.getSubtitle()));
+ MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(subtitle));
}
- if (description.getDescription() != null) {
+ @Nullable CharSequence displayDescription = description.getDescription();
+ if (displayDescription != null) {
builder.putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
- String.valueOf(description.getDescription()));
+ String.valueOf(displayDescription));
}
- if (description.getIconBitmap() != null) {
- builder.putBitmap(
- MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap());
+ @Nullable Bitmap iconBitmap = description.getIconBitmap();
+ if (iconBitmap != null) {
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, iconBitmap);
}
- if (description.getIconUri() != null) {
+ @Nullable Uri iconUri = description.getIconUri();
+ if (iconUri != null) {
builder.putString(
- MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
- String.valueOf(description.getIconUri()));
+ MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(iconUri));
}
- if (description.getMediaId() != null) {
- builder.putString(
- MediaMetadataCompat.METADATA_KEY_MEDIA_ID, description.getMediaId());
+ @Nullable String mediaId = description.getMediaId();
+ if (mediaId != null) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId);
}
- if (description.getMediaUri() != null) {
+ @Nullable Uri mediaUri = description.getMediaUri();
+ if (mediaUri != null) {
builder.putString(
- MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
- String.valueOf(description.getMediaUri()));
+ MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(mediaUri));
}
break;
}
@@ -975,8 +1022,8 @@ public final class MediaSessionConnector {
// Player.EventListener implementation.
@Override
- public void onTimelineChanged(
- Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
+ public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
+ Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) {
@@ -1019,6 +1066,7 @@ public final class MediaSessionConnector {
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
if (currentWindowIndex != player.getCurrentWindowIndex()) {
if (queueNavigator != null) {
queueNavigator.onCurrentWindowIndexChanged(player);
@@ -1045,19 +1093,20 @@ public final class MediaSessionConnector {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
- playbackPreparer.onPrepare();
+ playbackPreparer.onPrepare(/* playWhenReady= */ true);
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
- setPlayWhenReady(/* playWhenReady= */ true);
+ controlDispatcher.dispatchSetPlayWhenReady(
+ Assertions.checkNotNull(player), /* playWhenReady= */ true);
}
}
@Override
public void onPause() {
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) {
- setPlayWhenReady(/* playWhenReady= */ false);
+ controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);
}
}
@@ -1180,56 +1229,49 @@ public final class MediaSessionConnector {
@Override
public void onPrepare() {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
- setPlayWhenReady(/* playWhenReady= */ false);
- playbackPreparer.onPrepare();
+ playbackPreparer.onPrepare(/* playWhenReady= */ false);
}
}
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
- setPlayWhenReady(/* playWhenReady= */ false);
- playbackPreparer.onPrepareFromMediaId(mediaId, extras);
+ playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
- setPlayWhenReady(/* playWhenReady= */ false);
- playbackPreparer.onPrepareFromSearch(query, extras);
+ playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPrepareFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
- setPlayWhenReady(/* playWhenReady= */ false);
- playbackPreparer.onPrepareFromUri(uri, extras);
+ playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);
}
}
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
- setPlayWhenReady(/* playWhenReady= */ true);
- playbackPreparer.onPrepareFromMediaId(mediaId, extras);
+ playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromSearch(String query, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
- setPlayWhenReady(/* playWhenReady= */ true);
- playbackPreparer.onPrepareFromSearch(query, extras);
+ playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);
}
}
@Override
public void onPlayFromUri(Uri uri, Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
- setPlayWhenReady(/* playWhenReady= */ true);
- playbackPreparer.onPrepareFromUri(uri, extras);
+ playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, 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 617b8781f4..5c969dd44d 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
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.content.Context;
import android.os.Bundle;
+import androidx.annotation.Nullable;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player;
@@ -65,7 +66,7 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
@Override
public void onCustomAction(
- Player player, ControlDispatcher controlDispatcher, String action, Bundle extras) {
+ Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras) {
int mode = player.getRepeatMode();
int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
if (mode != proposedMode) {
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 d076404bb4..d72f6ffddc 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
@@ -166,7 +166,7 @@ public final class TimelineQueueEditor
@Override
public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {
- MediaSource mediaSource = sourceFactory.createMediaSource(description);
+ @Nullable MediaSource mediaSource = sourceFactory.createMediaSource(description);
if (mediaSource != null) {
queueDataAdapter.add(index, description);
queueMediaSource.addMediaSource(index, mediaSource);
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java
new file mode 100644
index 0000000000..65c0ce080e
--- /dev/null
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.mediasession;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle
index db2e073c8a..68bd422185 100644
--- a/extensions/okhttp/build.gradle
+++ b/extensions/okhttp/build.gradle
@@ -33,7 +33,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'androidx.annotation:annotation:1.0.2'
+ implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
api 'com.squareup.okhttp3:okhttp:3.12.1'
}
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 8eb8bba920..ec05c52f44 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
@@ -57,14 +57,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private final Call.Factory callFactory;
private final RequestProperties requestProperties;
- private final @Nullable String userAgent;
- private final @Nullable Predicate contentTypePredicate;
- private final @Nullable CacheControl cacheControl;
- private final @Nullable RequestProperties defaultRequestProperties;
+ @Nullable private final String userAgent;
+ @Nullable private final CacheControl cacheControl;
+ @Nullable private final RequestProperties defaultRequestProperties;
- private @Nullable DataSpec dataSpec;
- private @Nullable Response response;
- private @Nullable InputStream responseByteStream;
+ @Nullable private Predicate contentTypePredicate;
+ @Nullable private DataSpec dataSpec;
+ @Nullable private Response response;
+ @Nullable private InputStream responseByteStream;
private boolean opened;
private long bytesToSkip;
@@ -79,7 +79,28 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* @param userAgent An optional User-Agent string.
*/
public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) {
- this(callFactory, userAgent, /* contentTypePredicate= */ null);
+ this(callFactory, userAgent, /* cacheControl= */ null, /* defaultRequestProperties= */ null);
+ }
+
+ /**
+ * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
+ * by the source.
+ * @param userAgent An optional User-Agent string.
+ * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ */
+ public OkHttpDataSource(
+ Call.Factory callFactory,
+ @Nullable String userAgent,
+ @Nullable CacheControl cacheControl,
+ @Nullable RequestProperties defaultRequestProperties) {
+ super(/* isNetwork= */ true);
+ this.callFactory = Assertions.checkNotNull(callFactory);
+ this.userAgent = userAgent;
+ this.cacheControl = cacheControl;
+ this.defaultRequestProperties = defaultRequestProperties;
+ this.requestProperties = new RequestProperties();
}
/**
@@ -89,7 +110,10 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from {@link
* #open(DataSpec)}.
+ * @deprecated Use {@link #OkHttpDataSource(Call.Factory, String)} and {@link
+ * #setContentTypePredicate(Predicate)}.
*/
+ @Deprecated
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@@ -110,9 +134,12 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* predicate then a {@link InvalidContentTypeException} is thrown from {@link
* #open(DataSpec)}.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
- * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
- * the server as HTTP headers on every request.
+ * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
+ * server as HTTP headers on every request.
+ * @deprecated Use {@link #OkHttpDataSource(Call.Factory, String, CacheControl,
+ * RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
*/
+ @Deprecated
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@@ -128,8 +155,20 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
this.requestProperties = new RequestProperties();
}
+ /**
+ * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
+ * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
+ *
+ * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
+ * predicate that was previously set.
+ */
+ public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) {
+ this.contentTypePredicate = contentTypePredicate;
+ }
+
@Override
- public @Nullable Uri getUri() {
+ @Nullable
+ public Uri getUri() {
return response == null ? null : Uri.parse(response.request().url().toString());
}
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
index d0ef35cb07..f3d74f9233 100644
--- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java
@@ -29,9 +29,9 @@ import okhttp3.Call;
public final class OkHttpDataSourceFactory extends BaseFactory {
private final Call.Factory callFactory;
- private final @Nullable String userAgent;
- private final @Nullable TransferListener listener;
- private final @Nullable CacheControl cacheControl;
+ @Nullable private final String userAgent;
+ @Nullable private final TransferListener listener;
+ @Nullable private final CacheControl cacheControl;
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
@@ -89,7 +89,6 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
new OkHttpDataSource(
callFactory,
userAgent,
- /* contentTypePredicate= */ null,
cacheControl,
defaultRequestProperties);
if (listener != null) {
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java
new file mode 100644
index 0000000000..54eb4d5967
--- /dev/null
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.okhttp;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle
index 56acbdb7d3..7b621a8df9 100644
--- a/extensions/opus/build.gradle
+++ b/extensions/opus/build.gradle
@@ -39,7 +39,9 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ implementation 'androidx.annotation:annotation:1.1.0'
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
}
diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
index 7c6835db0b..382ee38e06 100644
--- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
+++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java
@@ -82,7 +82,7 @@ public class OpusPlaybackTest {
public void run() {
Looper.prepare();
LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer();
- DefaultTrackSelector trackSelector = new DefaultTrackSelector();
+ DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
MediaSource mediaSource =
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
index 59337c0847..2e9638c447 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.opus;
import android.os.Handler;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
@@ -25,20 +26,20 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes;
-/**
- * Decodes and renders audio using the native Opus decoder.
- */
-public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
+/** Decodes and renders audio using the native Opus decoder. */
+public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
/** The number of input and output buffers. */
private static final int NUM_BUFFERS = 16;
/** The default input buffer size. */
private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
- private OpusDecoder decoder;
+ @Nullable private OpusDecoder decoder;
+ private int channelCount;
+ private int sampleRate;
public LibopusAudioRenderer() {
- this(null, null);
+ this(/* eventHandler= */ null, /* eventListener= */ null);
}
/**
@@ -48,8 +49,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public LibopusAudioRenderer(
- Handler eventHandler,
- AudioRendererEventListener eventListener,
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
@@ -67,22 +68,30 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
- public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
- DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys,
+ public LibopusAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable DrmSessionManager drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys,
audioProcessors);
}
@Override
- protected int supportsFormatInternal(DrmSessionManager drmSessionManager,
- Format format) {
+ protected int supportsFormatInternal(
+ @Nullable DrmSessionManager drmSessionManager, Format format) {
+ boolean drmIsSupported =
+ format.drmInitData == null
+ || OpusLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType)
+ || (format.exoMediaCryptoType == null
+ && supportsFormatDrm(drmSessionManager, format.drmInitData));
if (!OpusLibrary.isAvailable()
|| !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
- } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
+ } else if (!drmIsSupported) {
return FORMAT_UNSUPPORTED_DRM;
} else {
return FORMAT_HANDLED;
@@ -90,7 +99,7 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
- protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
+ protected OpusDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws OpusDecoderException {
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
@@ -101,14 +110,25 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
initialInputBufferSize,
format.initializationData,
mediaCrypto);
+ channelCount = decoder.getChannelCount();
+ sampleRate = decoder.getSampleRate();
return decoder;
}
@Override
protected Format getOutputFormat() {
- return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
- Format.NO_VALUE, decoder.getChannelCount(), decoder.getSampleRate(), C.ENCODING_PCM_16BIT,
- null, null, 0, null);
+ return Format.createAudioSampleFormat(
+ /* id= */ null,
+ MimeTypes.AUDIO_RAW,
+ /* codecs= */ null,
+ Format.NO_VALUE,
+ Format.NO_VALUE,
+ channelCount,
+ sampleRate,
+ C.ENCODING_PCM_16BIT,
+ /* initializationData= */ null,
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null);
}
-
}
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
index f8ec477b88..d93036113c 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.opus;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
@@ -43,7 +44,7 @@ import java.util.List;
private static final int DECODE_ERROR = -1;
private static final int DRM_ERROR = -2;
- private final ExoMediaCrypto exoMediaCrypto;
+ @Nullable private final ExoMediaCrypto exoMediaCrypto;
private final int channelCount;
private final int headerSkipSamples;
@@ -65,8 +66,13 @@ import java.util.List;
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder.
*/
- public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
- List initializationData, ExoMediaCrypto exoMediaCrypto) throws OpusDecoderException {
+ public OpusDecoder(
+ int numInputBuffers,
+ int numOutputBuffers,
+ int initialInputBufferSize,
+ List initializationData,
+ @Nullable ExoMediaCrypto exoMediaCrypto)
+ throws OpusDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (!OpusLibrary.isAvailable()) {
throw new OpusDecoderException("Failed to load decoder native libraries.");
@@ -150,6 +156,7 @@ import java.util.List;
}
@Override
+ @Nullable
protected OpusDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
@@ -230,10 +237,22 @@ import java.util.List;
int gain, byte[] streamMap);
private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize,
SimpleOutputBuffer outputBuffer);
- private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer,
- int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate,
- ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
- int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
+
+ private native int opusSecureDecode(
+ long decoder,
+ long timeUs,
+ ByteBuffer inputBuffer,
+ int inputSize,
+ SimpleOutputBuffer outputBuffer,
+ int sampleRate,
+ @Nullable ExoMediaCrypto mediaCrypto,
+ int inputMode,
+ byte[] key,
+ byte[] iv,
+ int numSubSamples,
+ int[] numBytesOfClearData,
+ int[] numBytesOfEncryptedData);
+
private native void opusClose(long decoder);
private native void opusReset(long decoder);
private native int opusGetErrorCode(long decoder);
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
index 285be96388..d09d69bf03 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
@@ -15,8 +15,11 @@
*/
package com.google.android.exoplayer2.ext.opus;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.LibraryLoader;
+import com.google.android.exoplayer2.util.Util;
/**
* Configures and queries the underlying native library.
@@ -28,6 +31,7 @@ public final class OpusLibrary {
}
private static final LibraryLoader LOADER = new LibraryLoader("opusV2JNI");
+ @Nullable private static Class extends ExoMediaCrypto> exoMediaCryptoType;
private OpusLibrary() {}
@@ -36,10 +40,14 @@ public final class OpusLibrary {
* it must do so before calling any other method defined by this class, and before instantiating a
* {@link LibopusAudioRenderer} instance.
*
+ * @param exoMediaCryptoType The {@link ExoMediaCrypto} type expected for decoding protected
+ * content.
* @param libraries The names of the Opus native libraries.
*/
- public static void setLibraries(String... libraries) {
+ public static void setLibraries(
+ Class extends ExoMediaCrypto> exoMediaCryptoType, String... libraries) {
LOADER.setLibraries(libraries);
+ OpusLibrary.exoMediaCryptoType = exoMediaCryptoType;
}
/**
@@ -49,13 +57,21 @@ public final class OpusLibrary {
return LOADER.isAvailable();
}
- /**
- * Returns the version of the underlying library if available, or null otherwise.
- */
+ /** Returns the version of the underlying library if available, or null otherwise. */
+ @Nullable
public static String getVersion() {
return isAvailable() ? opusGetVersion() : null;
}
+ /**
+ * Returns whether the given {@link ExoMediaCrypto} type matches the one required for decoding
+ * protected content.
+ */
+ public static boolean matchesExpectedExoMediaCryptoType(
+ @Nullable Class extends ExoMediaCrypto> exoMediaCryptoType) {
+ return Util.areEqual(OpusLibrary.exoMediaCryptoType, exoMediaCryptoType);
+ }
+
public static native String opusGetVersion();
public static native boolean opusIsSecureDecodeSupported();
}
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java
new file mode 100644
index 0000000000..0848937fdc
--- /dev/null
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.opus;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/opus/src/test/AndroidManifest.xml b/extensions/opus/src/test/AndroidManifest.xml
index ac6a3bf68f..d17f889d17 100644
--- a/extensions/opus/src/test/AndroidManifest.xml
+++ b/extensions/opus/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle
index ca734c3657..74ef70fbf0 100644
--- a/extensions/rtmp/build.gradle
+++ b/extensions/rtmp/build.gradle
@@ -33,8 +33,9 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'net.butterflytv.utils:rtmp-client:3.0.1'
- implementation 'androidx.annotation:annotation:1.0.2'
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ implementation 'androidx.annotation:annotation:1.1.0'
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
index 272a8d1eb4..587e310d64 100644
--- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.rtmp;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
@@ -22,7 +24,6 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
-import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException;
import net.butterflytv.rtmp_client.RtmpClient;
import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException;
@@ -34,25 +35,13 @@ public final class RtmpDataSource extends BaseDataSource {
ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp");
}
- private RtmpClient rtmpClient;
- private Uri uri;
+ @Nullable private RtmpClient rtmpClient;
+ @Nullable private Uri uri;
public RtmpDataSource() {
super(/* isNetwork= */ true);
}
- /**
- * @param listener An optional listener.
- * @deprecated Use {@link #RtmpDataSource()} and {@link #addTransferListener(TransferListener)}.
- */
- @Deprecated
- public RtmpDataSource(@Nullable TransferListener listener) {
- this();
- if (listener != null) {
- addTransferListener(listener);
- }
- }
-
@Override
public long open(DataSpec dataSpec) throws RtmpIOException {
transferInitializing(dataSpec);
@@ -66,7 +55,7 @@ public final class RtmpDataSource extends BaseDataSource {
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
- int bytesRead = rtmpClient.read(buffer, offset, readLength);
+ int bytesRead = castNonNull(rtmpClient).read(buffer, offset, readLength);
if (bytesRead == -1) {
return C.RESULT_END_OF_INPUT;
}
@@ -87,6 +76,7 @@ public final class RtmpDataSource extends BaseDataSource {
}
@Override
+ @Nullable
public Uri getUri() {
return uri;
}
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
index 36abf825d6..505724e846 100644
--- a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java
@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.upstream.TransferListener;
*/
public final class RtmpDataSourceFactory implements DataSource.Factory {
- private final @Nullable TransferListener listener;
+ @Nullable private final TransferListener listener;
public RtmpDataSourceFactory() {
this(null);
diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java
new file mode 100644
index 0000000000..cb16630bd3
--- /dev/null
+++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.rtmp;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/rtmp/src/test/AndroidManifest.xml b/extensions/rtmp/src/test/AndroidManifest.xml
index 7eab4e2d59..b2e19827d9 100644
--- a/extensions/rtmp/src/test/AndroidManifest.xml
+++ b/extensions/rtmp/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md
index 0de29eea32..be75eae359 100644
--- a/extensions/vp9/README.md
+++ b/extensions/vp9/README.md
@@ -29,6 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
```
* Download the [Android NDK][] and set its location in an environment variable.
+ The build configuration has been tested with Android NDK r19c.
```
NDK_PATH=""
@@ -54,7 +55,7 @@ git checkout tags/v1.8.0 -b v1.8.0
```
cd ${VP9_EXT_PATH}/jni && \
-./generate_libvpx_android_configs.sh "${NDK_PATH}"
+./generate_libvpx_android_configs.sh
```
* Build the JNI native libraries from the command line:
@@ -66,7 +67,6 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
-[#3520]: https://github.com/google/ExoPlayer/issues/3520
## Notes ##
diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle
index fe1f220af6..3b8271869b 100644
--- a/extensions/vp9/build.gradle
+++ b/extensions/vp9/build.gradle
@@ -39,8 +39,9 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
- implementation 'androidx.annotation:annotation:1.0.2'
- testImplementation project(modulePrefix + 'testutils-robolectric')
+ implementation 'androidx.annotation:annotation:1.1.0'
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
index 5ebeca68d0..9be1d9c0e5 100644
--- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
+++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
@@ -115,7 +115,7 @@ public class VpxPlaybackTest {
public void run() {
Looper.prepare();
LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0);
- DefaultTrackSelector trackSelector = new DefaultTrackSelector();
+ DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector);
player.addListener(this);
MediaSource mediaSource =
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 d5da9a011d..b000ea1b6b 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
@@ -42,6 +42,7 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TimedValueQueue;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
@@ -123,7 +124,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private Format pendingFormat;
private Format outputFormat;
private VpxDecoder decoder;
- private VpxInputBuffer inputBuffer;
+ private VideoDecoderInputBuffer inputBuffer;
private VpxOutputBuffer outputBuffer;
@Nullable private DrmSession decoderDrmSession;
@Nullable private DrmSession sourceDrmSession;
@@ -136,7 +137,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private long joiningDeadlineMs;
private Surface surface;
private VpxOutputBufferRenderer outputBufferRenderer;
- private int outputMode;
+ @C.VideoOutputMode private int outputMode;
private boolean waitingForKeys;
private boolean inputStreamEnded;
@@ -173,8 +174,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*/
public LibvpxVideoRenderer(
long allowedJoiningTimeMs,
- Handler eventHandler,
- VideoRendererEventListener eventListener,
+ @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
this(
allowedJoiningTimeMs,
@@ -205,10 +206,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*/
public LibvpxVideoRenderer(
long allowedJoiningTimeMs,
- Handler eventHandler,
- VideoRendererEventListener eventListener,
+ @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify,
- DrmSessionManager drmSessionManager,
+ @Nullable DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys,
boolean disableLoopFilter) {
this(
@@ -248,10 +249,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*/
public LibvpxVideoRenderer(
long allowedJoiningTimeMs,
- Handler eventHandler,
- VideoRendererEventListener eventListener,
+ @Nullable Handler eventHandler,
+ @Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify,
- DrmSessionManager drmSessionManager,
+ @Nullable DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys,
boolean disableLoopFilter,
boolean enableRowMultiThreadMode,
@@ -274,7 +275,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
formatQueue = new TimedValueQueue<>();
flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
- outputMode = VpxDecoder.OUTPUT_MODE_NONE;
+ outputMode = C.VIDEO_OUTPUT_MODE_NONE;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
}
@@ -284,7 +285,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
public int supportsFormat(Format format) {
if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
- } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
+ }
+ boolean drmIsSupported =
+ format.drmInitData == null
+ || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType)
+ || (format.exoMediaCryptoType == null
+ && supportsFormatDrm(drmSessionManager, format.drmInitData));
+ if (!drmIsSupported) {
return FORMAT_UNSUPPORTED_DRM;
}
return FORMAT_HANDLED | ADAPTIVE_SEAMLESS;
@@ -301,7 +308,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
flagsOnlyBuffer.clear();
int result = readSource(formatHolder, flagsOnlyBuffer, true);
if (result == C.RESULT_FORMAT_READ) {
- onInputFormatChanged(formatHolder.format);
+ onInputFormatChanged(formatHolder);
} else if (result == C.RESULT_BUFFER_READ) {
// End of stream read having not read a format.
Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
@@ -342,8 +349,9 @@ public class LibvpxVideoRenderer extends BaseRenderer {
if (waitingForKeys) {
return false;
}
- if (format != null && (isSourceReady() || outputBuffer != null)
- && (renderedFirstFrame || outputMode == VpxDecoder.OUTPUT_MODE_NONE)) {
+ if (format != null
+ && (isSourceReady() || outputBuffer != null)
+ && (renderedFirstFrame || outputMode == C.VIDEO_OUTPUT_MODE_NONE)) {
// Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineMs = C.TIME_UNSET;
return true;
@@ -473,51 +481,46 @@ public class LibvpxVideoRenderer extends BaseRenderer {
}
private void setSourceDrmSession(@Nullable DrmSession session) {
- DrmSession previous = sourceDrmSession;
+ DrmSession.replaceSessionReferences(sourceDrmSession, session);
sourceDrmSession = session;
- releaseDrmSessionIfUnused(previous);
}
private void setDecoderDrmSession(@Nullable DrmSession session) {
- DrmSession previous = decoderDrmSession;
+ DrmSession.replaceSessionReferences(decoderDrmSession, session);
decoderDrmSession = session;
- releaseDrmSessionIfUnused(previous);
- }
-
- private void releaseDrmSessionIfUnused(@Nullable DrmSession session) {
- if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
- drmSessionManager.releaseSession(session);
- }
}
/**
* Called when a new format is read from the upstream source.
*
- * @param newFormat The new format.
+ * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}.
* @throws ExoPlaybackException If an error occurs (re-)initializing the decoder.
*/
@CallSuper
- protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+ @SuppressWarnings("unchecked")
+ protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
Format oldFormat = format;
- format = newFormat;
- pendingFormat = newFormat;
+ format = formatHolder.format;
+ pendingFormat = format;
boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null
: oldFormat.drmInitData);
if (drmInitDataChanged) {
if (format.drmInitData != null) {
- if (drmSessionManager == null) {
- throw ExoPlaybackException.createForRenderer(
- new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
+ if (formatHolder.includesDrmSession) {
+ setSourceDrmSession((DrmSession) formatHolder.drmSession);
+ } else {
+ if (drmSessionManager == null) {
+ throw ExoPlaybackException.createForRenderer(
+ new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
+ }
+ DrmSession session =
+ drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
+ if (sourceDrmSession != null) {
+ sourceDrmSession.releaseReference();
+ }
+ sourceDrmSession = session;
}
- 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 {
setSourceDrmSession(null);
}
@@ -544,7 +547,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*
* @param buffer The buffer that will be queued.
*/
- protected void onQueueInputBuffer(VpxInputBuffer buffer) {
+ protected void onQueueInputBuffer(VideoDecoderInputBuffer buffer) {
// Do nothing.
}
@@ -626,8 +629,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
*/
protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException {
int bufferMode = outputBuffer.mode;
- boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null;
- boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
+ boolean renderSurface = bufferMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && surface != null;
+ boolean renderYuv = bufferMode == C.VIDEO_OUTPUT_MODE_YUV && outputBufferRenderer != null;
lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000;
if (!renderYuv && !renderSurface) {
dropOutputBuffer(outputBuffer);
@@ -711,12 +714,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
this.surface = surface;
this.outputBufferRenderer = outputBufferRenderer;
if (surface != null) {
- outputMode = VpxDecoder.OUTPUT_MODE_SURFACE_YUV;
+ outputMode = C.VIDEO_OUTPUT_MODE_SURFACE_YUV;
} else {
outputMode =
- outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE;
+ outputBufferRenderer != null ? C.VIDEO_OUTPUT_MODE_YUV : C.VIDEO_OUTPUT_MODE_NONE;
}
- if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) {
+ if (outputMode != C.VIDEO_OUTPUT_MODE_NONE) {
if (decoder != null) {
decoder.setOutputMode(outputMode);
}
@@ -733,7 +736,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
clearReportedVideoSize();
clearRenderedFirstFrame();
}
- } else if (outputMode != VpxDecoder.OUTPUT_MODE_NONE) {
+ } else if (outputMode != C.VIDEO_OUTPUT_MODE_NONE) {
// The output is unchanged and non-null. If we know the video size and/or have already
// rendered to the output, report these again immediately.
maybeRenotifyVideoSizeChanged();
@@ -825,7 +828,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
return false;
}
if (result == C.RESULT_FORMAT_READ) {
- onInputFormatChanged(formatHolder.format);
+ onInputFormatChanged(formatHolder);
return true;
}
if (inputBuffer.isEndOfStream()) {
@@ -844,7 +847,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
pendingFormat = null;
}
inputBuffer.flip();
- inputBuffer.colorInfo = formatHolder.format.colorInfo;
+ inputBuffer.colorInfo = format.colorInfo;
onQueueInputBuffer(inputBuffer);
decoder.queueInputBuffer(inputBuffer);
buffersInCodecCount++;
@@ -913,7 +916,7 @@ public class LibvpxVideoRenderer extends BaseRenderer {
}
long earlyUs = outputBuffer.timeUs - positionUs;
- if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
+ if (outputMode == C.VIDEO_OUTPUT_MODE_NONE) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
if (isBufferLate(earlyUs)) {
skipOutputBuffer(outputBuffer);
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
index 57e5481b55..0efd4bd0ea 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
@@ -15,32 +15,28 @@
*/
package com.google.android.exoplayer2.ext.vp9;
+import androidx.annotation.Nullable;
import android.view.Surface;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DecryptionException;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import java.nio.ByteBuffer;
-/**
- * Vpx decoder.
- */
-/* package */ final class VpxDecoder extends
- SimpleDecoder {
-
- public static final int OUTPUT_MODE_NONE = -1;
- public static final int OUTPUT_MODE_YUV = 0;
- public static final int OUTPUT_MODE_SURFACE_YUV = 1;
+/** Vpx decoder. */
+/* package */ final class VpxDecoder
+ extends SimpleDecoder {
private static final int NO_ERROR = 0;
private static final int DECODE_ERROR = 1;
private static final int DRM_ERROR = 2;
- private final ExoMediaCrypto exoMediaCrypto;
+ @Nullable private final ExoMediaCrypto exoMediaCrypto;
private final long vpxDecContext;
- private volatile int outputMode;
+ @C.VideoOutputMode private volatile int outputMode;
/**
* Creates a VP9 decoder.
@@ -59,12 +55,12 @@ import java.nio.ByteBuffer;
int numInputBuffers,
int numOutputBuffers,
int initialInputBufferSize,
- ExoMediaCrypto exoMediaCrypto,
+ @Nullable ExoMediaCrypto exoMediaCrypto,
boolean disableLoopFilter,
boolean enableRowMultiThreadMode,
int threads)
throws VpxDecoderException {
- super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
+ super(new VideoDecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) {
throw new VpxDecoderException("Failed to load decoder native libraries.");
}
@@ -87,16 +83,15 @@ import java.nio.ByteBuffer;
/**
* Sets the output mode for frames rendered by the decoder.
*
- * @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE} and {@link
- * #OUTPUT_MODE_YUV}.
+ * @param outputMode The output mode.
*/
- public void setOutputMode(int outputMode) {
+ public void setOutputMode(@C.VideoOutputMode int outputMode) {
this.outputMode = outputMode;
}
@Override
- protected VpxInputBuffer createInputBuffer() {
- return new VpxInputBuffer();
+ protected VideoDecoderInputBuffer createInputBuffer() {
+ return new VideoDecoderInputBuffer();
}
@Override
@@ -108,7 +103,7 @@ import java.nio.ByteBuffer;
protected void releaseOutputBuffer(VpxOutputBuffer buffer) {
// Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
// require a call to vpxReleaseFrame.
- if (outputMode == OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
+ if (outputMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
vpxReleaseFrame(vpxDecContext, buffer);
}
super.releaseOutputBuffer(buffer);
@@ -120,8 +115,9 @@ import java.nio.ByteBuffer;
}
@Override
- protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
- boolean reset) {
+ @Nullable
+ protected VpxDecoderException decode(
+ VideoDecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) {
ByteBuffer inputData = inputBuffer.data;
int inputSize = inputData.limit();
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
@@ -174,9 +170,19 @@ import java.nio.ByteBuffer;
private native long vpxClose(long context);
private native long vpxDecode(long context, ByteBuffer encoded, int length);
- private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
- ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
- int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
+
+ private native long vpxSecureDecode(
+ long context,
+ ByteBuffer encoded,
+ int length,
+ @Nullable ExoMediaCrypto mediaCrypto,
+ int inputMode,
+ byte[] key,
+ byte[] iv,
+ int numSubSamples,
+ int[] numBytesOfClearData,
+ int[] numBytesOfEncryptedData);
+
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
/**
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
index 8de14629d3..b2da9a7ff8 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
@@ -15,8 +15,10 @@
*/
package com.google.android.exoplayer2.ext.vp9;
+import com.google.android.exoplayer2.video.VideoDecoderException;
+
/** Thrown when a libvpx decoder error occurs. */
-public final class VpxDecoderException extends Exception {
+public final class VpxDecoderException extends VideoDecoderException {
/* package */ VpxDecoderException(String message) {
super(message);
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
index 5a65fc56ff..e620332fc8 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
@@ -15,8 +15,11 @@
*/
package com.google.android.exoplayer2.ext.vp9;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.LibraryLoader;
+import com.google.android.exoplayer2.util.Util;
/**
* Configures and queries the underlying native library.
@@ -28,6 +31,7 @@ public final class VpxLibrary {
}
private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxV2JNI");
+ @Nullable private static Class extends ExoMediaCrypto> exoMediaCryptoType;
private VpxLibrary() {}
@@ -36,10 +40,14 @@ public final class VpxLibrary {
* it must do so before calling any other method defined by this class, and before instantiating a
* {@link LibvpxVideoRenderer} instance.
*
+ * @param exoMediaCryptoType The {@link ExoMediaCrypto} type required for decoding protected
+ * content.
* @param libraries The names of the Vpx native libraries.
*/
- public static void setLibraries(String... libraries) {
+ public static void setLibraries(
+ Class extends ExoMediaCrypto> exoMediaCryptoType, String... libraries) {
LOADER.setLibraries(libraries);
+ VpxLibrary.exoMediaCryptoType = exoMediaCryptoType;
}
/**
@@ -49,9 +57,8 @@ public final class VpxLibrary {
return LOADER.isAvailable();
}
- /**
- * Returns the version of the underlying library if available, or null otherwise.
- */
+ /** Returns the version of the underlying library if available, or null otherwise. */
+ @Nullable
public static String getVersion() {
return isAvailable() ? vpxGetVersion() : null;
}
@@ -60,6 +67,7 @@ public final class VpxLibrary {
* Returns the configuration string with which the underlying library was built if available, or
* null otherwise.
*/
+ @Nullable
public static String getBuildConfig() {
return isAvailable() ? vpxGetBuildConfig() : null;
}
@@ -74,6 +82,15 @@ public final class VpxLibrary {
return indexHbd >= 0;
}
+ /**
+ * Returns whether the given {@link ExoMediaCrypto} type matches the one required for decoding
+ * protected content.
+ */
+ public static boolean matchesExpectedExoMediaCryptoType(
+ @Nullable Class extends ExoMediaCrypto> exoMediaCryptoType) {
+ return Util.areEqual(VpxLibrary.exoMediaCryptoType, exoMediaCryptoType);
+ }
+
private static native String vpxGetVersion();
private static native String vpxGetBuildConfig();
public static native boolean vpxIsSecureDecodeSupported();
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
index 22330e0a05..7177cde12e 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java
@@ -15,37 +15,12 @@
*/
package com.google.android.exoplayer2.ext.vp9;
-import com.google.android.exoplayer2.decoder.OutputBuffer;
-import com.google.android.exoplayer2.video.ColorInfo;
-import java.nio.ByteBuffer;
+import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
-/** Output buffer containing video frame data, populated by {@link VpxDecoder}. */
-public final class VpxOutputBuffer extends OutputBuffer {
-
- public static final int COLORSPACE_UNKNOWN = 0;
- public static final int COLORSPACE_BT601 = 1;
- public static final int COLORSPACE_BT709 = 2;
- public static final int COLORSPACE_BT2020 = 3;
+/** Video output buffer, populated by {@link VpxDecoder}. */
+public final class VpxOutputBuffer extends VideoDecoderOutputBuffer {
private final VpxDecoder owner;
- /** Decoder private data. */
- public int decoderPrivate;
-
- public int mode;
- /**
- * RGB buffer for RGB mode.
- */
- public ByteBuffer data;
- public int width;
- public int height;
- public ColorInfo colorInfo;
-
- /**
- * YUV planes for YUV mode.
- */
- public ByteBuffer[] yuvPlanes;
- public int[] yuvStrides;
- public int colorspace;
public VpxOutputBuffer(VpxDecoder owner) {
this.owner = owner;
@@ -56,75 +31,4 @@ public final class VpxOutputBuffer extends OutputBuffer {
owner.releaseOutputBuffer(this);
}
- /**
- * Initializes the buffer.
- *
- * @param timeUs The presentation timestamp for the buffer, in microseconds.
- * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE} and {@link
- * VpxDecoder#OUTPUT_MODE_YUV}.
- */
- public void init(long timeUs, int mode) {
- this.timeUs = timeUs;
- this.mode = mode;
- }
-
- /**
- * Resizes the buffer based on the given stride. Called via JNI after decoding completes.
- *
- * @return Whether the buffer was resized successfully.
- */
- public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) {
- this.width = width;
- this.height = height;
- this.colorspace = colorspace;
- int uvHeight = (int) (((long) height + 1) / 2);
- if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) {
- return false;
- }
- int yLength = yStride * height;
- int uvLength = uvStride * uvHeight;
- int minimumYuvSize = yLength + (uvLength * 2);
- if (!isSafeToMultiply(uvLength, 2) || minimumYuvSize < yLength) {
- return false;
- }
- initData(minimumYuvSize);
-
- if (yuvPlanes == null) {
- yuvPlanes = new ByteBuffer[3];
- }
- // Rewrapping has to be done on every frame since the stride might have changed.
- yuvPlanes[0] = data.slice();
- yuvPlanes[0].limit(yLength);
- data.position(yLength);
- yuvPlanes[1] = data.slice();
- yuvPlanes[1].limit(uvLength);
- data.position(yLength + uvLength);
- yuvPlanes[2] = data.slice();
- yuvPlanes[2].limit(uvLength);
- if (yuvStrides == null) {
- yuvStrides = new int[3];
- }
- yuvStrides[0] = yStride;
- yuvStrides[1] = uvStride;
- yuvStrides[2] = uvStride;
- return true;
- }
-
- private void initData(int size) {
- if (data == null || data.capacity() < size) {
- data = ByteBuffer.allocateDirect(size);
- } else {
- data.position(0);
- data.limit(size);
- }
- }
-
- /**
- * Ensures that the result of multiplying individual numbers can fit into the size limit of an
- * integer.
- */
- private boolean isSafeToMultiply(int a, int b) {
- return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b);
- }
-
}
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java
index 8c765952e7..4e983cccc7 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxVideoSurfaceView.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.vp9;
import android.content.Context;
import android.opengl.GLSurfaceView;
+import androidx.annotation.Nullable;
import android.util.AttributeSet;
/**
@@ -27,10 +28,10 @@ public class VpxVideoSurfaceView extends GLSurfaceView implements VpxOutputBuffe
private final VpxRenderer renderer;
public VpxVideoSurfaceView(Context context) {
- this(context, null);
+ this(context, /* attrs= */ null);
}
- public VpxVideoSurfaceView(Context context, AttributeSet attrs) {
+ public VpxVideoSurfaceView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
renderer = new VpxRenderer();
setPreserveEGLContextOnPause(true);
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java
new file mode 100644
index 0000000000..b8725607a5
--- /dev/null
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.vp9;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/extensions/vp9/src/main/jni/Application.mk b/extensions/vp9/src/main/jni/Application.mk
index 59bf5f8f87..ed28f07acb 100644
--- a/extensions/vp9/src/main/jni/Application.mk
+++ b/extensions/vp9/src/main/jni/Application.mk
@@ -15,6 +15,6 @@
#
APP_OPTIM := release
-APP_STL := gnustl_static
+APP_STL := c++_static
APP_CPPFLAGS := -frtti
-APP_PLATFORM := android-9
+APP_PLATFORM := android-16
diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
index eab6862555..18f1dd5c69 100755
--- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
+++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
@@ -20,46 +20,33 @@
set -e
-if [ $# -ne 1 ]; then
- echo "Usage: ${0} "
+if [ $# -ne 0 ]; then
+ echo "Usage: ${0}"
exit
fi
-ndk="${1}"
-shift 1
-
# configuration parameters common to all architectures
common_params="--disable-examples --disable-docs --enable-realtime-only"
common_params+=" --disable-vp8 --disable-vp9-encoder --disable-webm-io"
common_params+=" --disable-libyuv --disable-runtime-cpu-detect"
+common_params+=" --enable-external-build"
# configuration parameters for various architectures
arch[0]="armeabi-v7a"
-config[0]="--target=armv7-android-gcc --sdk-path=$ndk --enable-neon"
-config[0]+=" --enable-neon-asm"
+config[0]="--target=armv7-android-gcc --enable-neon --enable-neon-asm"
-arch[1]="armeabi"
-config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon"
-config[1]+=" --disable-neon-asm"
+arch[1]="x86"
+config[1]="--force-target=x86-android-gcc --disable-sse2"
+config[1]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
+config[1]+=" --disable-avx2 --enable-pic"
-arch[2]="mips"
-config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk"
+arch[2]="arm64-v8a"
+config[2]="--force-target=armv8-android-gcc --enable-neon"
-arch[3]="x86"
-config[3]="--force-target=x86-android-gcc --sdk-path=$ndk --disable-sse2"
+arch[3]="x86_64"
+config[3]="--force-target=x86_64-android-gcc --disable-sse2"
config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
-config[3]+=" --disable-avx2 --enable-pic"
-
-arch[4]="arm64-v8a"
-config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon"
-
-arch[5]="x86_64"
-config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"
-config[5]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
-config[5]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm"
-
-arch[6]="mips64"
-config[6]="--force-target=mips64-android-gcc --sdk-path=$ndk"
+config[3]+=" --disable-avx2 --enable-pic --disable-neon --disable-neon-asm"
limit=$((${#arch[@]} - 1))
@@ -102,10 +89,7 @@ for i in $(seq 0 ${limit}); do
# configure and make
echo "build_android_configs: "
echo "configure ${config[${i}]} ${common_params}"
- ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \
- -isystem $ndk/sysroot/usr/include/arm-linux-androideabi \
- -isystem $ndk/sysroot/usr/include \
- "
+ ../../libvpx/configure ${config[${i}]} ${common_params}
rm -f libvpx_srcs.txt
for f in ${allowed_files}; do
# the build system supports multiple different configurations. avoid
diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc
index 82c023afbc..9fc8b09a18 100644
--- a/extensions/vp9/src/main/jni/vpx_jni.cc
+++ b/extensions/vp9/src/main/jni/vpx_jni.cc
@@ -60,6 +60,7 @@
// JNI references for VpxOutputBuffer class.
static jmethodID initForYuvFrame;
+static jmethodID initForPrivateFrame;
static jfieldID dataField;
static jfieldID outputModeField;
static jfieldID decoderPrivateField;
@@ -481,6 +482,8 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter,
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
"(IIIII)Z");
+ initForPrivateFrame =
+ env->GetMethodID(outputBufferClass, "initForPrivateFrame", "(II)V");
dataField = env->GetFieldID(outputBufferClass, "data",
"Ljava/nio/ByteBuffer;");
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
@@ -602,6 +605,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
}
jfb->d_w = img->d_w;
jfb->d_h = img->d_h;
+ env->CallVoidMethod(jOutputBuffer, initForPrivateFrame, img->d_w, img->d_h);
+ if (env->ExceptionCheck()) {
+ return -1;
+ }
env->SetIntField(jOutputBuffer, decoderPrivateField,
id + kDecoderPrivateBase);
}
diff --git a/extensions/vp9/src/test/AndroidManifest.xml b/extensions/vp9/src/test/AndroidManifest.xml
index a0123f17db..851213e653 100644
--- a/extensions/vp9/src/test/AndroidManifest.xml
+++ b/extensions/vp9/src/test/AndroidManifest.xml
@@ -14,4 +14,6 @@
limitations under the License.
-->
-
+
+
+
diff --git a/extensions/workmanager/README.md b/extensions/workmanager/README.md
new file mode 100644
index 0000000000..bd2dbc71ad
--- /dev/null
+++ b/extensions/workmanager/README.md
@@ -0,0 +1,22 @@
+# ExoPlayer WorkManager extension
+
+This extension provides a Scheduler implementation which uses [WorkManager][].
+
+[WorkManager]: https://developer.android.com/topic/libraries/architecture/workmanager.html
+
+## Getting the extension
+
+The easiest way to use the extension is to add it as a gradle dependency:
+
+```gradle
+implementation 'com.google.android.exoplayer:extension-workmanager:2.X.X'
+```
+
+where `2.X.X` is the version, which must match the version of the ExoPlayer
+library being used.
+
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle
new file mode 100644
index 0000000000..ea7564316f
--- /dev/null
+++ b/extensions/workmanager/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ defaultConfig {
+ minSdkVersion project.ext.minSdkVersion
+ targetSdkVersion project.ext.targetSdkVersion
+ }
+
+ testOptions.unitTests.includeAndroidResources = true
+}
+
+dependencies {
+ implementation project(modulePrefix + 'library-core')
+ implementation 'androidx.work:work-runtime:2.1.0'
+}
+
+ext {
+ javadocTitle = 'WorkManager extension'
+}
+apply from: '../../javadoc_library.gradle'
+
+ext {
+ releaseArtifact = 'extension-workmanager'
+ releaseDescription = 'WorkManager extension for ExoPlayer.'
+}
+apply from: '../../publish.gradle'
diff --git a/extensions/workmanager/src/main/AndroidManifest.xml b/extensions/workmanager/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..1daf50bd00
--- /dev/null
+++ b/extensions/workmanager/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java
new file mode 100644
index 0000000000..01801c9897
--- /dev/null
+++ b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java
@@ -0,0 +1,161 @@
+/*
+ * 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.ext.workmanager;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import androidx.work.Constraints;
+import androidx.work.Data;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.NetworkType;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkManager;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import com.google.android.exoplayer2.scheduler.Requirements;
+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.Util;
+
+/** A {@link Scheduler} that uses {@link WorkManager}. */
+public final class WorkManagerScheduler implements Scheduler {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "WorkManagerScheduler";
+ private static final String KEY_SERVICE_ACTION = "service_action";
+ private static final String KEY_SERVICE_PACKAGE = "service_package";
+ private static final String KEY_REQUIREMENTS = "requirements";
+
+ private final String workName;
+
+ /**
+ * @param workName A name for work scheduled by this instance. If the same name was used by a
+ * previous instance, anything scheduled by the previous instance will be canceled by this
+ * instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()} are
+ * called.
+ */
+ public WorkManagerScheduler(String workName) {
+ this.workName = workName;
+ }
+
+ @Override
+ public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
+ Constraints constraints = buildConstraints(requirements);
+ Data inputData = buildInputData(requirements, servicePackage, serviceAction);
+ OneTimeWorkRequest workRequest = buildWorkRequest(constraints, inputData);
+ logd("Scheduling work: " + workName);
+ WorkManager.getInstance().enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest);
+ return true;
+ }
+
+ @Override
+ public boolean cancel() {
+ logd("Canceling work: " + workName);
+ WorkManager.getInstance().cancelUniqueWork(workName);
+ return true;
+ }
+
+ private static Constraints buildConstraints(Requirements requirements) {
+ Constraints.Builder builder = new Constraints.Builder();
+
+ if (requirements.isUnmeteredNetworkRequired()) {
+ builder.setRequiredNetworkType(NetworkType.UNMETERED);
+ } else if (requirements.isNetworkRequired()) {
+ builder.setRequiredNetworkType(NetworkType.CONNECTED);
+ } else {
+ builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED);
+ }
+
+ if (requirements.isChargingRequired()) {
+ builder.setRequiresCharging(true);
+ }
+
+ if (requirements.isIdleRequired() && Util.SDK_INT >= 23) {
+ setRequiresDeviceIdle(builder);
+ }
+
+ return builder.build();
+ }
+
+ @TargetApi(23)
+ private static void setRequiresDeviceIdle(Constraints.Builder builder) {
+ builder.setRequiresDeviceIdle(true);
+ }
+
+ private static Data buildInputData(
+ Requirements requirements, String servicePackage, String serviceAction) {
+ Data.Builder builder = new Data.Builder();
+
+ builder.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
+ builder.putString(KEY_SERVICE_PACKAGE, servicePackage);
+ builder.putString(KEY_SERVICE_ACTION, serviceAction);
+
+ return builder.build();
+ }
+
+ private static OneTimeWorkRequest buildWorkRequest(Constraints constraints, Data inputData) {
+ OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SchedulerWorker.class);
+
+ builder.setConstraints(constraints);
+ builder.setInputData(inputData);
+
+ return builder.build();
+ }
+
+ private static void logd(String message) {
+ if (DEBUG) {
+ Log.d(TAG, message);
+ }
+ }
+
+ /** A {@link Worker} that starts the target service if the requirements are met. */
+ // This class needs to be public so that WorkManager can instantiate it.
+ public static final class SchedulerWorker extends Worker {
+
+ private final WorkerParameters workerParams;
+ private final Context context;
+
+ public SchedulerWorker(Context context, WorkerParameters workerParams) {
+ super(context, workerParams);
+ this.workerParams = workerParams;
+ this.context = context;
+ }
+
+ @Override
+ public Result doWork() {
+ logd("SchedulerWorker is started");
+ Data inputData = workerParams.getInputData();
+ Assertions.checkNotNull(inputData, "Work started without input data.");
+ Requirements requirements = new Requirements(inputData.getInt(KEY_REQUIREMENTS, 0));
+ if (requirements.checkRequirements(context)) {
+ logd("Requirements are met");
+ String serviceAction = inputData.getString(KEY_SERVICE_ACTION);
+ String servicePackage = inputData.getString(KEY_SERVICE_PACKAGE);
+ Assertions.checkNotNull(serviceAction, "Service action missing.");
+ Assertions.checkNotNull(servicePackage, "Service package missing.");
+ Intent intent = new Intent(serviceAction).setPackage(servicePackage);
+ logd("Starting service action: " + serviceAction + " package: " + servicePackage);
+ Util.startForegroundService(context, intent);
+ return Result.success();
+ } else {
+ logd("Requirements are not met");
+ return Result.retry();
+ }
+ }
+ }
+}
diff --git a/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/package-info.java b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/package-info.java
new file mode 100644
index 0000000000..7e0e244231
--- /dev/null
+++ b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.ext.workmanager;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/library/core/build.gradle b/library/core/build.gradle
index f532ae0e6a..fda2f079de 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -42,7 +42,6 @@ android {
}
test {
java.srcDirs += '../../testutils/src/main/java/'
- java.srcDirs += '../../testutils_robolectric/src/main/java/'
}
}
@@ -58,9 +57,12 @@ android {
}
dependencies {
- implementation 'androidx.annotation:annotation:1.0.2'
+ implementation 'androidx.annotation:annotation:1.1.0'
+ compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
+ // Uncomment to enable Kotlin non-null strict mode. See [internal: b/138703808].
+ // compileOnly "org.jetbrains.kotlin:kotlin-annotations-jvm:1.1.60"
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java
index 774f1b452c..bb14ac147b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java
@@ -94,11 +94,19 @@ public abstract class BasePlayer implements Player {
@Override
@Nullable
public final Object getCurrentTag() {
- int windowIndex = getCurrentWindowIndex();
Timeline timeline = getCurrentTimeline();
- return windowIndex >= timeline.getWindowCount()
+ return timeline.isEmpty()
? null
- : timeline.getWindow(windowIndex, window, /* setTag= */ true).tag;
+ : timeline.getWindow(getCurrentWindowIndex(), window, /* setTag= */ true).tag;
+ }
+
+ @Override
+ @Nullable
+ public final Object getCurrentManifest() {
+ Timeline timeline = getCurrentTimeline();
+ return timeline.isEmpty()
+ ? null
+ : timeline.getWindow(getCurrentWindowIndex(), window, /* setTag= */ false).manifest;
}
@Override
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 afe6a9879b..daa6124df6 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
@@ -71,9 +71,10 @@ public final class C {
/** Represents an unset or unknown percentage. */
public static final int PERCENTAGE_UNSET = -1;
- /**
- * The number of microseconds in one second.
- */
+ /** The number of milliseconds in one second. */
+ public static final long MILLIS_PER_SECOND = 1000L;
+
+ /** The number of microseconds in one second. */
public static final long MICROS_PER_SECOND = 1000000L;
/**
@@ -146,8 +147,8 @@ public final class C {
* {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
* #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link
- * #ENCODING_E_AC3}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or
- * {@link #ENCODING_DOLBY_TRUEHD}.
+ * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS},
+ * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@@ -163,6 +164,7 @@ public final class C {
ENCODING_PCM_A_LAW,
ENCODING_AC3,
ENCODING_E_AC3,
+ ENCODING_E_AC3_JOC,
ENCODING_AC4,
ENCODING_DTS,
ENCODING_DTS_HD,
@@ -210,6 +212,8 @@ public final class C {
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/** @see AudioFormat#ENCODING_E_AC3 */
public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
+ /** @see AudioFormat#ENCODING_E_AC3_JOC */
+ public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC;
/** @see AudioFormat#ENCODING_AC4 */
public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4;
/** @see AudioFormat#ENCODING_DTS */
@@ -496,6 +500,21 @@ public final class C {
/** Indicates that a buffer should be decoded but not rendered. */
public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000
+ /**
+ * Video decoder output modes. Possible modes are {@link #VIDEO_OUTPUT_MODE_NONE}, {@link
+ * #VIDEO_OUTPUT_MODE_YUV} and {@link #VIDEO_OUTPUT_MODE_SURFACE_YUV}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {VIDEO_OUTPUT_MODE_NONE, VIDEO_OUTPUT_MODE_YUV, VIDEO_OUTPUT_MODE_SURFACE_YUV})
+ public @interface VideoOutputMode {}
+ /** Video decoder output mode is not set. */
+ public static final int VIDEO_OUTPUT_MODE_NONE = -1;
+ /** Video decoder output mode that outputs raw 4:2:0 YUV planes. */
+ public static final int VIDEO_OUTPUT_MODE_YUV = 0;
+ /** Video decoder output mode that renders 4:2:0 YUV planes directly to a surface. */
+ public static final int VIDEO_OUTPUT_MODE_SURFACE_YUV = 1;
+
/**
* Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. One of {@link
* #VIDEO_SCALING_MODE_SCALE_TO_FIT} or {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java
index 89e7d857c8..1971a4cefc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java
@@ -32,19 +32,21 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
public interface PlaybackParameterListener {
/**
- * Called when the active playback parameters changed.
+ * Called when the active playback parameters changed. Will not be called for {@link
+ * #setPlaybackParameters(PlaybackParameters)}.
*
* @param newPlaybackParameters The newly active {@link PlaybackParameters}.
*/
void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters);
-
}
- private final StandaloneMediaClock standaloneMediaClock;
+ private final StandaloneMediaClock standaloneClock;
private final PlaybackParameterListener listener;
- private @Nullable Renderer rendererClockSource;
- private @Nullable MediaClock rendererClock;
+ @Nullable private Renderer rendererClockSource;
+ @Nullable private MediaClock rendererClock;
+ private boolean isUsingStandaloneClock;
+ private boolean standaloneClockIsStarted;
/**
* Creates a new instance with listener for playback parameter changes and a {@link Clock} to use
@@ -56,21 +58,24 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
*/
public DefaultMediaClock(PlaybackParameterListener listener, Clock clock) {
this.listener = listener;
- this.standaloneMediaClock = new StandaloneMediaClock(clock);
+ this.standaloneClock = new StandaloneMediaClock(clock);
+ isUsingStandaloneClock = true;
}
/**
* Starts the standalone fallback clock.
*/
public void start() {
- standaloneMediaClock.start();
+ standaloneClockIsStarted = true;
+ standaloneClock.start();
}
/**
* Stops the standalone fallback clock.
*/
public void stop() {
- standaloneMediaClock.stop();
+ standaloneClockIsStarted = false;
+ standaloneClock.stop();
}
/**
@@ -79,7 +84,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
* @param positionUs The position to set in microseconds.
*/
public void resetPosition(long positionUs) {
- standaloneMediaClock.resetPosition(positionUs);
+ standaloneClock.resetPosition(positionUs);
}
/**
@@ -99,8 +104,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
}
this.rendererClock = rendererMediaClock;
this.rendererClockSource = renderer;
- rendererClock.setPlaybackParameters(standaloneMediaClock.getPlaybackParameters());
- ensureSynced();
+ rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters());
}
}
@@ -114,65 +118,80 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
if (renderer == rendererClockSource) {
this.rendererClock = null;
this.rendererClockSource = null;
+ isUsingStandaloneClock = true;
}
}
/**
* Syncs internal clock if needed and returns current clock position in microseconds.
+ *
+ * @param isReadingAhead Whether the renderers are reading ahead.
*/
- public long syncAndGetPositionUs() {
- if (isUsingRendererClock()) {
- ensureSynced();
- return rendererClock.getPositionUs();
- } else {
- return standaloneMediaClock.getPositionUs();
- }
+ public long syncAndGetPositionUs(boolean isReadingAhead) {
+ syncClocks(isReadingAhead);
+ return getPositionUs();
}
// MediaClock implementation.
@Override
public long getPositionUs() {
- if (isUsingRendererClock()) {
- return rendererClock.getPositionUs();
- } else {
- return standaloneMediaClock.getPositionUs();
- }
+ return isUsingStandaloneClock ? standaloneClock.getPositionUs() : rendererClock.getPositionUs();
}
@Override
- public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
if (rendererClock != null) {
- playbackParameters = rendererClock.setPlaybackParameters(playbackParameters);
+ rendererClock.setPlaybackParameters(playbackParameters);
+ playbackParameters = rendererClock.getPlaybackParameters();
}
- standaloneMediaClock.setPlaybackParameters(playbackParameters);
- listener.onPlaybackParametersChanged(playbackParameters);
- return playbackParameters;
+ standaloneClock.setPlaybackParameters(playbackParameters);
}
@Override
public PlaybackParameters getPlaybackParameters() {
- return rendererClock != null ? rendererClock.getPlaybackParameters()
- : standaloneMediaClock.getPlaybackParameters();
+ return rendererClock != null
+ ? rendererClock.getPlaybackParameters()
+ : standaloneClock.getPlaybackParameters();
}
- private void ensureSynced() {
+ private void syncClocks(boolean isReadingAhead) {
+ if (shouldUseStandaloneClock(isReadingAhead)) {
+ isUsingStandaloneClock = true;
+ if (standaloneClockIsStarted) {
+ standaloneClock.start();
+ }
+ return;
+ }
long rendererClockPositionUs = rendererClock.getPositionUs();
- standaloneMediaClock.resetPosition(rendererClockPositionUs);
+ if (isUsingStandaloneClock) {
+ // Ensure enabling the renderer clock doesn't jump backwards in time.
+ if (rendererClockPositionUs < standaloneClock.getPositionUs()) {
+ standaloneClock.stop();
+ return;
+ }
+ isUsingStandaloneClock = false;
+ if (standaloneClockIsStarted) {
+ standaloneClock.start();
+ }
+ }
+ // Continuously sync stand-alone clock to renderer clock so that it can take over if needed.
+ standaloneClock.resetPosition(rendererClockPositionUs);
PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters();
- if (!playbackParameters.equals(standaloneMediaClock.getPlaybackParameters())) {
- standaloneMediaClock.setPlaybackParameters(playbackParameters);
+ if (!playbackParameters.equals(standaloneClock.getPlaybackParameters())) {
+ standaloneClock.setPlaybackParameters(playbackParameters);
listener.onPlaybackParametersChanged(playbackParameters);
}
}
- private boolean isUsingRendererClock() {
- // Use the renderer clock if the providing renderer has not ended or needs the next sample
- // stream to reenter the ready state. The latter case uses the standalone clock to avoid getting
- // stuck if tracks in the current period have uneven durations.
- // See: https://github.com/google/ExoPlayer/issues/1874.
- return rendererClockSource != null && !rendererClockSource.isEnded()
- && (rendererClockSource.isReady() || !rendererClockSource.hasReadStreamToEnd());
+ private boolean shouldUseStandaloneClock(boolean isReadingAhead) {
+ // Use the standalone clock if the clock providing renderer is not set or has ended. Also use
+ // the standalone clock if the renderer is not ready and we have finished reading the stream or
+ // are reading ahead to avoid getting stuck if tracks in the current period have uneven
+ // durations. See: https://github.com/google/ExoPlayer/issues/1874.
+ return rendererClockSource == null
+ || rendererClockSource.isEnded()
+ || (!rendererClockSource.isReady()
+ && (isReadingAhead || rendererClockSource.hasReadStreamToEnd()));
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
index 2a977f5bba..490d961396 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
@@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
@@ -90,6 +91,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
@ExtensionRendererMode private int extensionRendererMode;
private long allowedVideoJoiningTimeMs;
private boolean playClearSamplesWithoutKeys;
+ private boolean enableDecoderFallback;
private MediaCodecSelector mediaCodecSelector;
/** @param context A {@link Context}. */
@@ -202,6 +204,19 @@ public class DefaultRenderersFactory implements RenderersFactory {
return this;
}
+ /**
+ * Sets whether to enable fallback to lower-priority decoders if decoder initialization fails.
+ * This may result in using a decoder that is less efficient or slower than the primary decoder.
+ *
+ * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
+ * initialization fails.
+ * @return This factory, for convenience.
+ */
+ public DefaultRenderersFactory setEnableDecoderFallback(boolean enableDecoderFallback) {
+ this.enableDecoderFallback = enableDecoderFallback;
+ return this;
+ }
+
/**
* Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers.
*
@@ -248,6 +263,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
mediaCodecSelector,
drmSessionManager,
playClearSamplesWithoutKeys,
+ enableDecoderFallback,
eventHandler,
videoRendererEventListener,
allowedVideoJoiningTimeMs,
@@ -258,6 +274,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
mediaCodecSelector,
drmSessionManager,
playClearSamplesWithoutKeys,
+ enableDecoderFallback,
buildAudioProcessors(),
eventHandler,
audioRendererEventListener,
@@ -282,6 +299,9 @@ public class DefaultRenderersFactory implements RenderersFactory {
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
* the media.
+ * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
+ * initialization fails. This may result in using a decoder that is slower/less efficient than
+ * the primary decoder.
* @param eventHandler A handler associated with the main thread's looper.
* @param eventListener An event listener.
* @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to
@@ -294,6 +314,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys,
+ boolean enableDecoderFallback,
Handler eventHandler,
VideoRendererEventListener eventListener,
long allowedVideoJoiningTimeMs,
@@ -305,6 +326,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
allowedVideoJoiningTimeMs,
drmSessionManager,
playClearSamplesWithoutKeys,
+ enableDecoderFallback,
eventHandler,
eventListener,
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
@@ -356,6 +378,9 @@ public class DefaultRenderersFactory implements RenderersFactory {
* @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of
* encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of
* the media.
+ * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
+ * initialization fails. This may result in using a decoder that is slower/less efficient than
+ * the primary decoder.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers
* before output. May be empty.
* @param eventHandler A handler to use when invoking event listeners and outputs.
@@ -368,6 +393,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
MediaCodecSelector mediaCodecSelector,
@Nullable DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys,
+ boolean enableDecoderFallback,
AudioProcessor[] audioProcessors,
Handler eventHandler,
AudioRendererEventListener eventListener,
@@ -378,10 +404,10 @@ public class DefaultRenderersFactory implements RenderersFactory {
mediaCodecSelector,
drmSessionManager,
playClearSamplesWithoutKeys,
+ enableDecoderFallback,
eventHandler,
eventListener,
- AudioCapabilities.getCapabilities(context),
- audioProcessors));
+ new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)));
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
return;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
index b5f8f954bb..49aacd9638 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2;
+import android.os.SystemClock;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource;
@@ -73,6 +74,9 @@ public final class ExoPlaybackException extends Exception {
*/
public final int rendererIndex;
+ /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */
+ public final long timestampMs;
+
@Nullable private final Throwable cause;
/**
@@ -131,6 +135,7 @@ public final class ExoPlaybackException extends Exception {
this.type = type;
this.cause = cause;
this.rendererIndex = rendererIndex;
+ timestampMs = SystemClock.elapsedRealtime();
}
private ExoPlaybackException(@Type int type, String message) {
@@ -138,6 +143,7 @@ public final class ExoPlaybackException extends Exception {
this.type = type;
rendererIndex = C.INDEX_UNSET;
cause = null;
+ timestampMs = SystemClock.elapsedRealtime();
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
index d0f9e2ae04..ee29af9c99 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -117,30 +117,6 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
*/
public interface ExoPlayer extends Player {
- /** @deprecated Use {@link PlayerMessage.Target} instead. */
- @Deprecated
- interface ExoPlayerComponent extends PlayerMessage.Target {}
-
- /** @deprecated Use {@link PlayerMessage} instead. */
- @Deprecated
- final class ExoPlayerMessage {
-
- /** The target to receive the message. */
- public final PlayerMessage.Target target;
- /** The type of the message. */
- public final int messageType;
- /** The message. */
- public final Object message;
-
- /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */
- @Deprecated
- public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) {
- this.target = target;
- this.messageType = messageType;
- this.message = message;
- }
- }
-
/** Returns the {@link Looper} associated with the playback thread. */
Looper getPlaybackLooper();
@@ -181,19 +157,6 @@ public interface ExoPlayer extends Player {
*/
PlayerMessage createMessage(PlayerMessage.Target target);
- /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */
- @Deprecated
- @SuppressWarnings("deprecation")
- void sendMessages(ExoPlayerMessage... messages);
-
- /**
- * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link
- * PlayerMessage#blockUntilDelivered()}.
- */
- @Deprecated
- @SuppressWarnings("deprecation")
- void blockingSendMessages(ExoPlayerMessage... messages);
-
/**
* Sets the parameters that control how seek operations are performed.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
index 59647feaa9..9168f1bd76 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
@@ -102,7 +102,7 @@ public final class ExoPlayerFactory {
* @param context A {@link Context}.
*/
public static SimpleExoPlayer newSimpleInstance(Context context) {
- return newSimpleInstance(context, new DefaultTrackSelector());
+ return newSimpleInstance(context, new DefaultTrackSelector(context));
}
/**
@@ -244,7 +244,7 @@ public final class ExoPlayerFactory {
loadControl,
drmSessionManager,
bandwidthMeter,
- new AnalyticsCollector.Factory(),
+ new AnalyticsCollector(Clock.DEFAULT),
Util.getLooper());
}
@@ -257,8 +257,8 @@ public final class ExoPlayerFactory {
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
- * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
- * will collect and forward all player events.
+ * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all
+ * player events.
*/
public static SimpleExoPlayer newSimpleInstance(
Context context,
@@ -266,14 +266,14 @@ public final class ExoPlayerFactory {
TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
- AnalyticsCollector.Factory analyticsCollectorFactory) {
+ AnalyticsCollector analyticsCollector) {
return newSimpleInstance(
context,
renderersFactory,
trackSelector,
loadControl,
drmSessionManager,
- analyticsCollectorFactory,
+ analyticsCollector,
Util.getLooper());
}
@@ -302,7 +302,7 @@ public final class ExoPlayerFactory {
trackSelector,
loadControl,
drmSessionManager,
- new AnalyticsCollector.Factory(),
+ new AnalyticsCollector(Clock.DEFAULT),
looper);
}
@@ -315,8 +315,8 @@ public final class ExoPlayerFactory {
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
- * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
- * will collect and forward all player events.
+ * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all
+ * player events.
* @param looper The {@link Looper} which must be used for all calls to the player and which is
* used to call listeners on.
*/
@@ -326,7 +326,7 @@ public final class ExoPlayerFactory {
TrackSelector trackSelector,
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
- AnalyticsCollector.Factory analyticsCollectorFactory,
+ AnalyticsCollector analyticsCollector,
Looper looper) {
return newSimpleInstance(
context,
@@ -335,7 +335,7 @@ public final class ExoPlayerFactory {
loadControl,
drmSessionManager,
getDefaultBandwidthMeter(context),
- analyticsCollectorFactory,
+ analyticsCollector,
looper);
}
@@ -348,8 +348,8 @@ public final class ExoPlayerFactory {
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
- * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that
- * will collect and forward all player events.
+ * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all
+ * player events.
* @param looper The {@link Looper} which must be used for all calls to the player and which is
* used to call listeners on.
*/
@@ -360,7 +360,7 @@ public final class ExoPlayerFactory {
LoadControl loadControl,
@Nullable DrmSessionManager drmSessionManager,
BandwidthMeter bandwidthMeter,
- AnalyticsCollector.Factory analyticsCollectorFactory,
+ AnalyticsCollector analyticsCollector,
Looper looper) {
return new SimpleExoPlayer(
context,
@@ -369,7 +369,7 @@ public final class ExoPlayerFactory {
loadControl,
drmSessionManager,
bandwidthMeter,
- analyticsCollectorFactory,
+ analyticsCollector,
looper);
}
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 bea7af189a..e99429d3b2 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
@@ -35,8 +35,6 @@ import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/** An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}. */
@@ -71,9 +69,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
private boolean hasPendingPrepare;
private boolean hasPendingSeek;
private boolean foregroundMode;
+ private int pendingSetPlaybackParametersAcks;
private PlaybackParameters playbackParameters;
private SeekParameters seekParameters;
- private @Nullable ExoPlaybackException playbackError;
+ @Nullable private ExoPlaybackException playbackError;
// Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo;
@@ -199,7 +198,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
}
@Override
- public @Nullable ExoPlaybackException getPlaybackError() {
+ @Nullable
+ public ExoPlaybackException getPlaybackError() {
return playbackError;
}
@@ -337,7 +337,14 @@ import java.util.concurrent.CopyOnWriteArrayList;
if (playbackParameters == null) {
playbackParameters = PlaybackParameters.DEFAULT;
}
+ if (this.playbackParameters.equals(playbackParameters)) {
+ return;
+ }
+ pendingSetPlaybackParametersAcks++;
+ this.playbackParameters = playbackParameters;
internalPlayer.setPlaybackParameters(playbackParameters);
+ PlaybackParameters playbackParametersToNotify = playbackParameters;
+ notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParametersToNotify));
}
@Override
@@ -409,15 +416,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
/* playbackState= */ Player.STATE_IDLE);
}
- @Override
- @Deprecated
- @SuppressWarnings("deprecation")
- public void sendMessages(ExoPlayerMessage... messages) {
- for (ExoPlayerMessage message : messages) {
- createMessage(message.target).setType(message.messageType).setPayload(message.message).send();
- }
- }
-
@Override
public PlayerMessage createMessage(Target target) {
return new PlayerMessage(
@@ -428,36 +426,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
internalPlayerHandler);
}
- @Override
- @Deprecated
- @SuppressWarnings("deprecation")
- public void blockingSendMessages(ExoPlayerMessage... messages) {
- List playerMessages = new ArrayList<>();
- for (ExoPlayerMessage message : messages) {
- playerMessages.add(
- createMessage(message.target)
- .setType(message.messageType)
- .setPayload(message.message)
- .send());
- }
- boolean wasInterrupted = false;
- for (PlayerMessage message : playerMessages) {
- boolean blockMessage = true;
- while (blockMessage) {
- try {
- message.blockUntilDelivered();
- blockMessage = false;
- } catch (InterruptedException e) {
- wasInterrupted = true;
- }
- }
- }
- if (wasInterrupted) {
- // Restore the interrupted status.
- Thread.currentThread().interrupt();
- }
- }
-
@Override
public int getCurrentPeriodIndex() {
if (shouldMaskPosition()) {
@@ -511,7 +479,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
@Override
public long getTotalBufferedDuration() {
- return Math.max(0, C.usToMs(playbackInfo.totalBufferedDurationUs));
+ return C.usToMs(playbackInfo.totalBufferedDurationUs);
}
@Override
@@ -533,7 +501,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
public long getContentPosition() {
if (isPlayingAd()) {
playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
- return period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs);
+ return playbackInfo.contentPositionUs == C.TIME_UNSET
+ ? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs()
+ : period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs);
} else {
return getCurrentPosition();
}
@@ -587,11 +557,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
return playbackInfo.timeline;
}
- @Override
- public Object getCurrentManifest() {
- return playbackInfo.manifest;
- }
-
// Not private so it can be called from an inner class without going through a thunk method.
/* package */ void handleEvent(Message msg) {
switch (msg.what) {
@@ -603,11 +568,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
/* positionDiscontinuityReason= */ msg.arg2);
break;
case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED:
- PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj;
- if (!this.playbackParameters.equals(playbackParameters)) {
- this.playbackParameters = playbackParameters;
- notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters));
- }
+ handlePlaybackParameters((PlaybackParameters) msg.obj, /* operationAck= */ msg.arg1 != 0);
break;
case ExoPlayerImplInternal.MSG_ERROR:
ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj;
@@ -619,6 +580,19 @@ import java.util.concurrent.CopyOnWriteArrayList;
}
}
+ private void handlePlaybackParameters(
+ PlaybackParameters playbackParameters, boolean operationAck) {
+ if (operationAck) {
+ pendingSetPlaybackParametersAcks--;
+ }
+ if (pendingSetPlaybackParametersAcks == 0) {
+ if (!this.playbackParameters.equals(playbackParameters)) {
+ this.playbackParameters = playbackParameters;
+ notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters));
+ }
+ }
+ }
+
private void handlePlaybackInfo(
PlaybackInfo playbackInfo,
int operationAcks,
@@ -673,13 +647,12 @@ import java.util.concurrent.CopyOnWriteArrayList;
resetPosition = resetPosition || resetState;
MediaPeriodId mediaPeriodId =
resetPosition
- ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window)
+ ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)
: playbackInfo.periodId;
long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs;
long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
return new PlaybackInfo(
resetState ? Timeline.EMPTY : playbackInfo.timeline,
- resetState ? null : playbackInfo.manifest,
mediaPeriodId,
startPositionUs,
contentPositionUs,
@@ -753,7 +726,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
private final @Player.TimelineChangeReason int timelineChangeReason;
private final boolean seekProcessed;
private final boolean playbackStateChanged;
- private final boolean timelineOrManifestChanged;
+ private final boolean timelineChanged;
private final boolean isLoadingChanged;
private final boolean trackSelectorResultChanged;
private final boolean playWhenReady;
@@ -777,9 +750,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
this.seekProcessed = seekProcessed;
this.playWhenReady = playWhenReady;
playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState;
- timelineOrManifestChanged =
- previousPlaybackInfo.timeline != playbackInfo.timeline
- || previousPlaybackInfo.manifest != playbackInfo.manifest;
+ timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline;
isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;
trackSelectorResultChanged =
previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
@@ -787,12 +758,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
@Override
public void run() {
- if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
+ if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
invokeAll(
listenerSnapshot,
- listener ->
- listener.onTimelineChanged(
- playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason));
+ listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason));
}
if (positionDiscontinuity) {
invokeAll(
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 34d8d0aa08..6ab0838e26 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
@@ -29,6 +29,7 @@ import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
@@ -51,7 +52,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
implements Handler.Callback,
MediaPeriod.Callback,
TrackSelector.InvalidationListener,
- MediaSource.SourceInfoRefreshListener,
+ MediaSourceCaller,
PlaybackParameterListener,
PlayerMessage.Sender {
@@ -264,12 +265,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
return internalPlaybackThread.getLooper();
}
- // MediaSource.SourceInfoRefreshListener implementation.
+ // MediaSource.MediaSourceCaller implementation.
@Override
- public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) {
- handler.obtainMessage(MSG_REFRESH_SOURCE_INFO,
- new MediaSourceRefreshInfo(source, timeline, manifest)).sendToTarget();
+ public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
+ handler
+ .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline))
+ .sendToTarget();
}
// MediaPeriod.Callback implementation.
@@ -295,9 +297,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
- handler
- .obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, playbackParameters)
- .sendToTarget();
+ sendPlaybackParametersChangedInternal(playbackParameters, /* acknowledgeCommand= */ false);
}
// Handler.Callback implementation.
@@ -356,7 +356,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
reselectTracksInternal();
break;
case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL:
- handlePlaybackParameters((PlaybackParameters) msg.obj);
+ handlePlaybackParameters(
+ (PlaybackParameters) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0);
break;
case MSG_SEND_MESSAGE:
sendMessageInternal((PlayerMessage) msg.obj);
@@ -440,7 +441,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
loadControl.onPrepared();
this.mediaSource = mediaSource;
setState(Player.STATE_BUFFERING);
- mediaSource.prepareSource(/* listener= */ this, bandwidthMeter.getTransferListener());
+ mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener());
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
@@ -534,7 +535,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
}
} else {
- rendererPositionUs = mediaClock.syncAndGetPositionUs();
+ rendererPositionUs =
+ mediaClock.syncAndGetPositionUs(
+ /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod());
periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs);
playbackInfo.positionUs = periodPositionUs;
@@ -643,7 +646,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (resolvedSeekPosition == null) {
// The seek position was valid for the timeline that it was performed into, but the
// timeline has changed or is not ready and a suitable seek position could not be resolved.
- periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window);
+ periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period);
periodPositionUs = C.TIME_UNSET;
contentPositionUs = C.TIME_UNSET;
seekPositionAdjusted = true;
@@ -728,13 +731,20 @@ import java.util.concurrent.atomic.AtomicBoolean;
newPlayingPeriodHolder = queue.advancePlayingPeriod();
}
- // Disable all the renderers if the period being played is changing, or if forced.
- if (oldPlayingPeriodHolder != newPlayingPeriodHolder || forceDisableRenderers) {
+ // Disable all renderers if the period being played is changing, if the seek results in negative
+ // renderer timestamps, or if forced.
+ if (forceDisableRenderers
+ || oldPlayingPeriodHolder != newPlayingPeriodHolder
+ || (newPlayingPeriodHolder != null
+ && newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) {
for (Renderer renderer : enabledRenderers) {
disableRenderer(renderer);
}
enabledRenderers = new Renderer[0];
oldPlayingPeriodHolder = null;
+ if (newPlayingPeriodHolder != null) {
+ newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0);
+ }
}
// Update the holders.
@@ -774,6 +784,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
mediaClock.setPlaybackParameters(playbackParameters);
+ sendPlaybackParametersChangedInternal(
+ mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true);
}
private void setSeekParametersInternal(SeekParameters seekParameters) {
@@ -872,7 +884,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
- queue.clear(/* keepFrontPeriodUid= */ !resetPosition);
+ queue.clear(/* keepFrontPeriodUid= */ !resetState);
setIsLoading(false);
if (resetState) {
queue.setTimeline(Timeline.EMPTY);
@@ -884,7 +896,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
MediaPeriodId mediaPeriodId =
resetPosition
- ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window)
+ ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)
: playbackInfo.periodId;
// Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored.
long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs;
@@ -892,7 +904,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfo =
new PlaybackInfo(
resetState ? Timeline.EMPTY : playbackInfo.timeline,
- resetState ? null : playbackInfo.manifest,
mediaPeriodId,
startPositionUs,
contentPositionUs,
@@ -906,7 +917,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
startPositionUs);
if (releaseMediaSource) {
if (mediaSource != null) {
- mediaSource.releaseSource(/* listener= */ this);
+ mediaSource.releaseSource(/* caller= */ this);
mediaSource = null;
}
}
@@ -1097,7 +1108,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
return;
}
newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline);
- if (newTrackSelectorResult != null) {
+ if (!newTrackSelectorResult.isEquivalent(periodHolder.getTrackSelectorResult())) {
// Selected tracks have changed for this period.
break;
}
@@ -1186,13 +1197,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void notifyTrackSelectionDiscontinuity() {
MediaPeriodHolder periodHolder = queue.getFrontPeriod();
while (periodHolder != null) {
- TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult();
- if (trackSelectorResult != null) {
- TrackSelection[] trackSelections = trackSelectorResult.selections.getAll();
- for (TrackSelection trackSelection : trackSelections) {
- if (trackSelection != null) {
- trackSelection.onDiscontinuity();
- }
+ TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
+ for (TrackSelection trackSelection : trackSelections) {
+ if (trackSelection != null) {
+ trackSelection.onDiscontinuity();
}
}
periodHolder = periodHolder.getNext();
@@ -1269,9 +1277,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
Timeline oldTimeline = playbackInfo.timeline;
Timeline timeline = sourceRefreshInfo.timeline;
- Object manifest = sourceRefreshInfo.manifest;
queue.setTimeline(timeline);
- playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest);
+ playbackInfo = playbackInfo.copyWithTimeline(timeline);
resolvePendingMessagePositions();
MediaPeriodId newPeriodId = playbackInfo.periodId;
@@ -1296,8 +1303,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
Pair