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 download This 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 errorMessageProvider; - private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost; + @Nullable private ErrorMessageProvider 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 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 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 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 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 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 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 defaultPosition = getPeriodPosition( timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); - newContentPositionUs = defaultPosition.second; - newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs); + newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second); + if (!newPeriodId.isAd()) { + // Keep unset start position if we need to play an ad first. + newContentPositionUs = defaultPosition.second; + } } else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) { // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose // window we can restart from. @@ -1479,82 +1489,67 @@ import java.util.concurrent.atomic.AtomicBoolean; mediaSource.maybeThrowSourceInfoRefreshError(); return; } - - // Update the loading period if required. maybeUpdateLoadingPeriod(); + maybeUpdateReadingPeriod(); + maybeUpdatePlayingPeriod(); + } + + private void maybeUpdateLoadingPeriod() throws IOException { + queue.reevaluateBuffer(rendererPositionUs); + if (queue.shouldLoadNextMediaPeriod()) { + MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo); + if (info == null) { + maybeThrowSourceInfoRefreshError(); + } else { + MediaPeriod mediaPeriod = + queue.enqueueNextMediaPeriod( + rendererCapabilities, + trackSelector, + loadControl.getAllocator(), + mediaSource, + info, + emptyTrackSelectorResult); + mediaPeriod.prepare(this, info.startPositionUs); + setIsLoading(true); + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + } + } MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); } else if (!playbackInfo.isLoading) { maybeContinueLoading(); } + } - if (!queue.hasPlayingPeriod()) { - // We're waiting for the first period to be prepared. + private void maybeUpdateReadingPeriod() throws ExoPlaybackException, IOException { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (readingPeriodHolder == null) { return; } - // Advance the playing period if necessary. - MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); - boolean advancedPlayingPeriod = false; - while (playWhenReady - && playingPeriodHolder != readingPeriodHolder - && rendererPositionUs >= playingPeriodHolder.getNext().getStartPositionRendererTime()) { - // All enabled renderers' streams have been read to the end, and the playback position reached - // the end of the playing period, so advance playback to the next period. - if (advancedPlayingPeriod) { - // If we advance more than one period at a time, notify listeners after each update. - maybeNotifyPlaybackInfoChanged(); - } - int discontinuityReason = - playingPeriodHolder.info.isLastInTimelinePeriod - ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION - : Player.DISCONTINUITY_REASON_AD_INSERTION; - MediaPeriodHolder oldPlayingPeriodHolder = playingPeriodHolder; - playingPeriodHolder = queue.advancePlayingPeriod(); - updatePlayingPeriodRenderers(oldPlayingPeriodHolder); - playbackInfo = - playbackInfo.copyWithNewPosition( - playingPeriodHolder.info.id, - playingPeriodHolder.info.startPositionUs, - playingPeriodHolder.info.contentPositionUs, - getTotalBufferedDurationUs()); - playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); - updatePlaybackPositions(); - advancedPlayingPeriod = true; - } - - if (readingPeriodHolder.info.isFinal) { - for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; - // Defer setting the stream as final until the renderer has actually consumed the whole - // stream in case of playlist changes that cause the stream to be no longer final. - if (sampleStream != null && renderer.getStream() == sampleStream - && renderer.hasReadStreamToEnd()) { - renderer.setCurrentStreamFinal(); + if (readingPeriodHolder.getNext() == null) { + // We don't have a successor to advance the reading period to. + if (readingPeriodHolder.info.isFinal) { + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; + // Defer setting the stream as final until the renderer has actually consumed the whole + // stream in case of playlist changes that cause the stream to be no longer final. + if (sampleStream != null + && renderer.getStream() == sampleStream + && renderer.hasReadStreamToEnd()) { + renderer.setCurrentStreamFinal(); + } } } return; } - // Advance the reading period if necessary. - if (readingPeriodHolder.getNext() == null) { - // We don't have a successor to advance the reading period to. + if (!hasReadingPeriodFinishedReading()) { return; } - for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; - if (renderer.getStream() != sampleStream - || (sampleStream != null && !renderer.hasReadStreamToEnd())) { - // The current reading period is still being read by at least one renderer. - return; - } - } - if (!readingPeriodHolder.getNext().prepared) { // The successor is not prepared yet. maybeThrowPeriodPrepareError(); @@ -1565,18 +1560,18 @@ import java.util.concurrent.atomic.AtomicBoolean; readingPeriodHolder = queue.advanceReadingPeriod(); TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); - boolean initialDiscontinuity = - readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; + if (readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) { + // The new period starts with a discontinuity, so the renderers will play out all data, then + // be disabled and re-enabled when they start playing the next period. + setAllRendererStreamsFinal(); + return; + } for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; boolean rendererWasEnabled = oldTrackSelectorResult.isRendererEnabled(i); - if (!rendererWasEnabled) { - // The renderer was disabled and will be enabled when we play the next period. - } else if (initialDiscontinuity) { - // The new period starts with a discontinuity, so the renderer will play out all data then - // be disabled and re-enabled when it starts playing the next period. - renderer.setCurrentStreamFinal(); - } else if (!renderer.isCurrentStreamFinal()) { + if (rendererWasEnabled && !renderer.isCurrentStreamFinal()) { + // The renderer is enabled and its stream is not final, so we still have a chance to replace + // the sample streams. TrackSelection newSelection = newTrackSelectorResult.selections.get(i); boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i); boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE; @@ -1604,23 +1599,67 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private void maybeUpdateLoadingPeriod() throws IOException { - queue.reevaluateBuffer(rendererPositionUs); - if (queue.shouldLoadNextMediaPeriod()) { - MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo); - if (info == null) { - maybeThrowSourceInfoRefreshError(); - } else { - MediaPeriod mediaPeriod = - queue.enqueueNextMediaPeriod( - rendererCapabilities, - trackSelector, - loadControl.getAllocator(), - mediaSource, - info); - mediaPeriod.prepare(this, info.startPositionUs); - setIsLoading(true); - handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { + boolean advancedPlayingPeriod = false; + while (shouldAdvancePlayingPeriod()) { + if (advancedPlayingPeriod) { + // If we advance more than one period at a time, notify listeners after each update. + maybeNotifyPlaybackInfoChanged(); + } + MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod(); + updatePlayingPeriodRenderers(oldPlayingPeriodHolder); + playbackInfo = + playbackInfo.copyWithNewPosition( + newPlayingPeriodHolder.info.id, + newPlayingPeriodHolder.info.startPositionUs, + newPlayingPeriodHolder.info.contentPositionUs, + getTotalBufferedDurationUs()); + int discontinuityReason = + oldPlayingPeriodHolder.info.isLastInTimelinePeriod + ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + : Player.DISCONTINUITY_REASON_AD_INSERTION; + playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); + updatePlaybackPositions(); + advancedPlayingPeriod = true; + } + } + + private boolean shouldAdvancePlayingPeriod() { + if (!playWhenReady) { + return false; + } + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + if (playingPeriodHolder == null) { + return false; + } + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (playingPeriodHolder == readingPeriodHolder) { + return false; + } + MediaPeriodHolder nextPlayingPeriodHolder = + Assertions.checkNotNull(playingPeriodHolder.getNext()); + return rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime(); + } + + private boolean hasReadingPeriodFinishedReading() { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; + if (renderer.getStream() != sampleStream + || (sampleStream != null && !renderer.hasReadStreamToEnd())) { + // The current reading period is still being read by at least one renderer. + return false; + } + } + return true; + } + + private void setAllRendererStreamsFinal() { + for (Renderer renderer : renderers) { + if (renderer.getStream() != null) { + renderer.setCurrentStreamFinal(); } } } @@ -1653,9 +1692,13 @@ import java.util.concurrent.atomic.AtomicBoolean; maybeContinueLoading(); } - private void handlePlaybackParameters(PlaybackParameters playbackParameters) + private void handlePlaybackParameters( + PlaybackParameters playbackParameters, boolean acknowledgeCommand) throws ExoPlaybackException { - eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + eventHandler + .obtainMessage( + MSG_PLAYBACK_PARAMETERS_CHANGED, acknowledgeCommand ? 1 : 0, 0, playbackParameters) + .sendToTarget(); updateTrackSelectionPlaybackSpeed(playbackParameters.speed); for (Renderer renderer : renderers) { if (renderer != null) { @@ -1797,9 +1840,12 @@ import java.util.concurrent.atomic.AtomicBoolean; private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) { MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); - return loadingPeriodHolder == null - ? 0 - : bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs); + if (loadingPeriodHolder == null) { + return 0; + } + long totalBufferedDurationUs = + bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs); + return Math.max(0, totalBufferedDurationUs); } private void updateLoadControlTrackSelection( @@ -1807,6 +1853,17 @@ import java.util.concurrent.atomic.AtomicBoolean; loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections); } + private void sendPlaybackParametersChangedInternal( + PlaybackParameters playbackParameters, boolean acknowledgeCommand) { + handler + .obtainMessage( + MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, + acknowledgeCommand ? 1 : 0, + 0, + playbackParameters) + .sendToTarget(); + } + private static Format[] getFormats(TrackSelection newSelection) { // Build an array of formats contained by the selection. int length = newSelection != null ? newSelection.length() : 0; @@ -1836,7 +1893,7 @@ import java.util.concurrent.atomic.AtomicBoolean; public int resolvedPeriodIndex; public long resolvedPeriodTimeUs; - public @Nullable Object resolvedPeriodUid; + @Nullable public Object resolvedPeriodUid; public PendingMessageInfo(PlayerMessage message) { this.message = message; @@ -1871,12 +1928,10 @@ import java.util.concurrent.atomic.AtomicBoolean; public final MediaSource source; public final Timeline timeline; - public final Object manifest; - public MediaSourceRefreshInfo(MediaSource source, Timeline timeline, Object manifest) { + public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) { this.source = source; this.timeline = timeline; - this.manifest = manifest; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index a90435227b..f420f20767 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.10.1"; + public static final String VERSION = "2.10.4"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.4"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2010001; + public static final int VERSION_INT = 2010004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index dcb7a83dca..b2bd20f0fe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -19,6 +19,8 @@ import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -45,9 +47,9 @@ public final class Format implements Parcelable { public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE; /** An identifier for the format, or null if unknown or not applicable. */ - public final @Nullable String id; + @Nullable public final String id; /** The human readable label, or null if unknown or not applicable. */ - public final @Nullable String label; + @Nullable public final String label; /** Track selection flags. */ @C.SelectionFlags public final int selectionFlags; /** Track role flags. */ @@ -57,14 +59,14 @@ public final class Format implements Parcelable { */ public final int bitrate; /** Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */ - public final @Nullable String codecs; + @Nullable public final String codecs; /** Metadata, or null if unknown or not applicable. */ - public final @Nullable Metadata metadata; + @Nullable public final Metadata metadata; // Container specific. /** The mime type of the container, or null if unknown or not applicable. */ - public final @Nullable String containerMimeType; + @Nullable public final String containerMimeType; // Elementary stream specific. @@ -72,7 +74,7 @@ public final class Format implements Parcelable { * The mime type of the elementary stream (i.e. the individual samples), or null if unknown or not * applicable. */ - public final @Nullable String sampleMimeType; + @Nullable public final String sampleMimeType; /** * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or * not applicable. @@ -84,7 +86,7 @@ public final class Format implements Parcelable { */ public final List initializationData; /** DRM initialization data if the stream is protected, or null otherwise. */ - public final @Nullable DrmInitData drmInitData; + @Nullable public final DrmInitData drmInitData; /** * For samples that contain subsamples, this is an offset that should be added to subsample @@ -122,9 +124,9 @@ public final class Format implements Parcelable { @C.StereoMode public final int stereoMode; /** The projection data for 360/VR video, or null if not applicable. */ - public final @Nullable byte[] projectionData; + @Nullable public final byte[] projectionData; /** The color metadata associated with the video, helps with accurate color reproduction. */ - public final @Nullable ColorInfo colorInfo; + @Nullable public final ColorInfo colorInfo; // Audio specific. @@ -157,17 +159,30 @@ public final class Format implements Parcelable { // Audio and text specific. /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */ - public final @Nullable String language; + @Nullable public final String language; /** * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */ public final int accessibilityChannel; + // Provided by source. + + /** + * The type of the {@link ExoMediaCrypto} provided by the media source, if the media source can + * acquire a {@link DrmSession} for {@link #drmInitData}. Null if the media source cannot acquire + * a session for {@link #drmInitData}, or if not applicable. + */ + @Nullable public final Class exoMediaCryptoType; + // Lazily initialized hashcode. private int hashCode; // Video. + /** + * @deprecated Use {@link #createVideoContainerFormat(String, String, String, String, String, int, + * int, int, float, List, int, int)} instead. + */ @Deprecated public static Format createVideoContainerFormat( @Nullable String id, @@ -236,7 +251,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createVideoSampleFormat( @@ -340,11 +356,16 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Audio. + /** + * @deprecated Use {@link #createAudioContainerFormat(String, String, String, String, String, int, + * int, int, List, int, int, String)} instead. + */ @Deprecated public static Format createAudioContainerFormat( @Nullable String id, @@ -413,7 +434,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createAudioSampleFormat( @@ -518,7 +540,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Text. @@ -585,7 +608,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - accessibilityChannel); + accessibilityChannel, + /* exoMediaCryptoType= */ null); } public static Format createTextSampleFormat( @@ -698,7 +722,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - accessibilityChannel); + accessibilityChannel, + /* exoMediaCryptoType= */ null); } // Image. @@ -740,11 +765,16 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Generic. + /** + * @deprecated Use {@link #createContainerFormat(String, String, String, String, String, int, int, + * int, String)} instead. + */ @Deprecated public static Format createContainerFormat( @Nullable String id, @@ -804,7 +834,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createSampleFormat( @@ -837,7 +868,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createSampleFormat( @@ -874,7 +906,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } /* package */ Format( @@ -910,7 +943,9 @@ public final class Format implements Parcelable { int encoderPadding, // Audio and text specific. @Nullable String language, - int accessibilityChannel) { + int accessibilityChannel, + // Provided by source. + @Nullable Class exoMediaCryptoType) { this.id = id; this.label = label; this.selectionFlags = selectionFlags; @@ -946,6 +981,8 @@ public final class Format implements Parcelable { // Audio and text specific. this.language = Util.normalizeLanguageCode(language); this.accessibilityChannel = accessibilityChannel; + // Provided by source. + this.exoMediaCryptoType = exoMediaCryptoType; } @SuppressWarnings("ResourceType") @@ -988,6 +1025,8 @@ public final class Format implements Parcelable { // Audio and text specific. language = in.readString(); accessibilityChannel = in.readInt(); + // Provided by source. + exoMediaCryptoType = null; } public Format copyWithMaxInputSize(int maxInputSize) { @@ -1019,7 +1058,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { @@ -1051,7 +1091,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithContainerInfo( @@ -1099,7 +1140,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } @SuppressWarnings("ReferenceEquality") @@ -1178,7 +1220,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { @@ -1210,7 +1253,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithFrameRate(float frameRate) { @@ -1242,42 +1286,24 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel); + return copyWithAdjustments(drmInitData, metadata); } public Format copyWithMetadata(@Nullable Metadata metadata) { + return copyWithAdjustments(drmInitData, metadata); + } + + @SuppressWarnings("ReferenceEquality") + public Format copyWithAdjustments( + @Nullable DrmInitData drmInitData, @Nullable Metadata metadata) { + if (drmInitData == this.drmInitData && metadata == this.metadata) { + return this; + } return new Format( id, label, @@ -1306,7 +1332,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithRotationDegrees(int rotationDegrees) { @@ -1338,7 +1365,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithBitrate(int bitrate) { @@ -1370,7 +1398,74 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); + } + + public Format copyWithVideoSize(int width, int height) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); + } + + public Format copyWithExoMediaCryptoType(Class exoMediaCryptoType) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); } /** @@ -1449,6 +1544,8 @@ public final class Format implements Parcelable { // Audio and text specific. result = 31 * result + (language == null ? 0 : language.hashCode()); result = 31 * result + accessibilityChannel; + // Provided by source. + result = 31 * result + (exoMediaCryptoType == null ? 0 : exoMediaCryptoType.hashCode()); hashCode = result; } return hashCode; @@ -1484,6 +1581,7 @@ public final class Format implements Parcelable { && accessibilityChannel == other.accessibilityChannel && Float.compare(frameRate, other.frameRate) == 0 && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 + && Util.areEqual(exoMediaCryptoType, other.exoMediaCryptoType) && Util.areEqual(id, other.id) && Util.areEqual(label, other.label) && Util.areEqual(codecs, other.codecs) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java index 5da8d0f9f5..c1b2ace9b7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/FormatHolder.java @@ -16,23 +16,20 @@ package com.google.android.exoplayer2; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.drm.DecryptionResource; +import com.google.android.exoplayer2.drm.DrmSession; /** * Holds a {@link Format}. */ public final class FormatHolder { - /** - * Whether the object expected to populate {@link #format} is also expected to populate {@link - * #decryptionResource}. - */ + /** Whether the {@link #format} setter also sets the {@link #drmSession} field. */ // TODO: Remove once all Renderers and MediaSources have migrated to the new DRM model [Internal // ref: b/129764794]. - public boolean decryptionResourceIsProvided; + public boolean includesDrmSession; /** An accompanying context for decrypting samples in the format. */ - @Nullable public DecryptionResource decryptionResource; + @Nullable public DrmSession drmSession; /** The held {@link Format}. */ @Nullable public Format format; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index 5950bc627f..850d2b7d10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -59,20 +59,21 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final MediaSource mediaSource; @Nullable private MediaPeriodHolder next; - @Nullable private TrackGroupArray trackGroups; - @Nullable private TrackSelectorResult trackSelectorResult; + private TrackGroupArray trackGroups; + private TrackSelectorResult trackSelectorResult; private long rendererPositionOffsetUs; /** * Creates a new holder with information required to play it as part of a timeline. * * @param rendererCapabilities The renderer capabilities. - * @param rendererPositionOffsetUs The time offset of the start of the media period to provide to - * renderers. + * @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds. * @param trackSelector The track selector. * @param allocator The allocator. * @param mediaSource The media source that produced the media period. * @param info Information used to identify this media period in its timeline period. + * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each + * renderer. */ public MediaPeriodHolder( RendererCapabilities[] rendererCapabilities, @@ -80,13 +81,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; TrackSelector trackSelector, Allocator allocator, MediaSource mediaSource, - MediaPeriodInfo info) { + MediaPeriodInfo info, + TrackSelectorResult emptyTrackSelectorResult) { this.rendererCapabilities = rendererCapabilities; - this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs; + this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.trackSelector = trackSelector; this.mediaSource = mediaSource; this.uid = info.id.periodUid; this.info = info; + this.trackGroups = TrackGroupArray.EMPTY; + this.trackSelectorResult = emptyTrackSelectorResult; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; mediaPeriod = @@ -115,6 +119,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return rendererPositionOffsetUs; } + /** + * Sets the renderer time of the start of the period, in microseconds. + * + * @param rendererPositionOffsetUs The new renderer position offset, in microseconds. + */ + public void setRendererOffset(long rendererPositionOffsetUs) { + this.rendererPositionOffsetUs = rendererPositionOffsetUs; + } + /** Returns start position of period in renderer time. */ public long getStartPositionRendererTime() { return info.startPositionUs + rendererPositionOffsetUs; @@ -159,8 +172,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public void handlePrepared(float playbackSpeed, Timeline timeline) throws ExoPlaybackException { prepared = true; trackGroups = mediaPeriod.getTrackGroups(); - TrackSelectorResult selectorResult = - Assertions.checkNotNull(selectTracks(playbackSpeed, timeline)); + TrackSelectorResult selectorResult = selectTracks(playbackSpeed, timeline); long newStartPositionUs = applyTrackSelection( selectorResult, info.startPositionUs, /* forceRecreateStreams= */ false); @@ -194,22 +206,20 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } /** - * Selects tracks for the period and returns the new result if the selection changed. Must only be - * called if {@link #prepared} is {@code true}. + * Selects tracks for the period. Must only be called if {@link #prepared} is {@code true}. + * + *

The new track selection needs to be applied with {@link + * #applyTrackSelection(TrackSelectorResult, long, boolean)} before taking effect. * * @param playbackSpeed The current playback speed. * @param timeline The current {@link Timeline}. - * @return The {@link TrackSelectorResult} if the result changed. Or null if nothing changed. + * @return The {@link TrackSelectorResult}. * @throws ExoPlaybackException If an error occurs during track selection. */ - @Nullable public TrackSelectorResult selectTracks(float playbackSpeed, Timeline timeline) throws ExoPlaybackException { TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities, getTrackGroups(), info.id, timeline); - if (selectorResult.isEquivalent(trackSelectorResult)) { - return null; - } for (TrackSelection trackSelection : selectorResult.selections.getAll()) { if (trackSelection != null) { trackSelection.onPlaybackSpeed(playbackSpeed); @@ -295,7 +305,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Releases the media period. No other method should be called after the release. */ public void release() { disableTrackSelectionsInResult(); - trackSelectorResult = null; releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod); } @@ -323,25 +332,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return next; } - /** - * Returns the {@link TrackGroupArray} exposed by this media period. Must only be called if {@link - * #prepared} is {@code true}. - */ + /** Returns the {@link TrackGroupArray} exposed by this media period. */ public TrackGroupArray getTrackGroups() { - return Assertions.checkNotNull(trackGroups); + return trackGroups; } - /** - * Returns the {@link TrackSelectorResult} which is currently applied. Must only be called if - * {@link #prepared} is {@code true}. - */ + /** Returns the {@link TrackSelectorResult} which is currently applied. */ public TrackSelectorResult getTrackSelectorResult() { - return Assertions.checkNotNull(trackSelectorResult); + return trackSelectorResult; } private void enableTrackSelectionsInResult() { - TrackSelectorResult trackSelectorResult = this.trackSelectorResult; - if (!isLoadingMediaPeriod() || trackSelectorResult == null) { + if (!isLoadingMediaPeriod()) { return; } for (int i = 0; i < trackSelectorResult.length; i++) { @@ -354,8 +356,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void disableTrackSelectionsInResult() { - TrackSelectorResult trackSelectorResult = this.trackSelectorResult; - if (!isLoadingMediaPeriod() || trackSelectorResult == null) { + if (!isLoadingMediaPeriod()) { return; } for (int i = 0; i < trackSelectorResult.length; i++) { @@ -386,7 +387,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ private void associateNoSampleRenderersWithEmptySampleStream( @NullableType SampleStream[] sampleStreams) { - TrackSelectorResult trackSelectorResult = Assertions.checkNotNull(this.trackSelectorResult); for (int i = 0; i < rendererCapabilities.length; i++) { if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE && trackSelectorResult.isRendererEnabled(i)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java index bc1ea7b1e1..2733df7ba6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java @@ -29,7 +29,8 @@ import com.google.android.exoplayer2.util.Util; public final long startPositionUs; /** * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} - * otherwise. + * if this is not an ad or the next content media period should be played from its default + * position. */ public final long contentPositionUs; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 249548340e..0f279ba6d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -22,6 +22,7 @@ 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.trackselection.TrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -46,11 +47,11 @@ import com.google.android.exoplayer2.util.Assertions; private Timeline timeline; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private @Nullable MediaPeriodHolder playing; - private @Nullable MediaPeriodHolder reading; - private @Nullable MediaPeriodHolder loading; + @Nullable private MediaPeriodHolder playing; + @Nullable private MediaPeriodHolder reading; + @Nullable private MediaPeriodHolder loading; private int length; - private @Nullable Object oldFrontPeriodUid; + @Nullable private Object oldFrontPeriodUid; private long oldFrontPeriodWindowSequenceNumber; /** Creates a new media period queue. */ @@ -135,17 +136,22 @@ import com.google.android.exoplayer2.util.Assertions; * @param allocator The allocator. * @param mediaSource The media source that produced the media period. * @param info Information used to identify this media period in its timeline period. + * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each + * renderer. */ public MediaPeriod enqueueNextMediaPeriod( RendererCapabilities[] rendererCapabilities, TrackSelector trackSelector, Allocator allocator, MediaSource mediaSource, - MediaPeriodInfo info) { + MediaPeriodInfo info, + TrackSelectorResult emptyTrackSelectorResult) { long rendererPositionOffsetUs = loading == null - ? info.startPositionUs - : (loading.getRendererOffset() + loading.info.durationUs); + ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET + ? info.contentPositionUs + : 0) + : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs); MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder( rendererCapabilities, @@ -153,7 +159,8 @@ import com.google.android.exoplayer2.util.Assertions; trackSelector, allocator, mediaSource, - info); + info, + emptyTrackSelectorResult); if (loading != null) { Assertions.checkState(hasPlayingPeriod()); loading.setNext(newPeriodHolder); @@ -168,6 +175,7 @@ import com.google.android.exoplayer2.util.Assertions; * Returns the loading period holder which is at the end of the queue, or null if the queue is * empty. */ + @Nullable public MediaPeriodHolder getLoadingPeriod() { return loading; } @@ -176,6 +184,7 @@ import com.google.android.exoplayer2.util.Assertions; * Returns the playing period holder which is at the front of the queue, or null if the queue is * empty or hasn't started playing. */ + @Nullable public MediaPeriodHolder getPlayingPeriod() { return playing; } @@ -184,6 +193,7 @@ import com.google.android.exoplayer2.util.Assertions; * Returns the reading period holder, or null if the queue is empty or the player hasn't started * reading. */ + @Nullable public MediaPeriodHolder getReadingPeriod() { return reading; } @@ -192,6 +202,7 @@ import com.google.android.exoplayer2.util.Assertions; * Returns the period holder in the front of the queue which is the playing period holder when * playing, or null if the queue is empty. */ + @Nullable public MediaPeriodHolder getFrontPeriod() { return hasPlayingPeriod() ? playing : loading; } @@ -219,6 +230,7 @@ import com.google.android.exoplayer2.util.Assertions; * * @return The updated playing period holder, or null if the queue is or becomes empty. */ + @Nullable public MediaPeriodHolder advancePlayingPeriod() { if (playing != null) { if (playing == reading) { @@ -275,8 +287,8 @@ import com.google.android.exoplayer2.util.Assertions; if (front != null) { oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null; oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber; - front.release(); removeAfter(front); + front.release(); } else if (!keepFrontPeriodUid) { oldFrontPeriodUid = null; } @@ -560,6 +572,7 @@ import com.google.android.exoplayer2.util.Assertions; } long startPositionUs; + long contentPositionUs; int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex; Object nextPeriodUid = period.uid; @@ -568,6 +581,7 @@ import com.google.android.exoplayer2.util.Assertions; // We're starting to buffer a new window. When playback transitions to this window we'll // want it to be from its default start position, so project the default start position // forward by the duration of the buffer, and start buffering from this point. + contentPositionUs = C.TIME_UNSET; Pair defaultPosition = timeline.getPeriodPosition( window, @@ -587,12 +601,13 @@ import com.google.android.exoplayer2.util.Assertions; windowSequenceNumber = nextWindowSequenceNumber++; } } else { + // We're starting to buffer a new period within the same window. startPositionUs = 0; + contentPositionUs = 0; } MediaPeriodId periodId = resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber); - return getMediaPeriodInfo( - periodId, /* contentPositionUs= */ startPositionUs, startPositionUs); + return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs); } MediaPeriodId currentPeriodId = mediaPeriodInfo.id; @@ -616,13 +631,11 @@ import com.google.android.exoplayer2.util.Assertions; mediaPeriodInfo.contentPositionUs, currentPeriodId.windowSequenceNumber); } else { - // Play content from the ad group position. As a special case, if we're transitioning from a - // preroll ad group to content and there are no other ad groups, project the start position - // forward as if this were a transition to a new window. No attempt is made to handle - // midrolls in live streams, as it's unclear what content position should play after an ad - // (server-side dynamic ad insertion is more appropriate for this use case). + // Play content from the ad group position. long startPositionUs = mediaPeriodInfo.contentPositionUs; - if (period.getAdGroupCount() == 1 && period.getAdGroupTimeUs(0) == 0) { + if (startPositionUs == C.TIME_UNSET) { + // If we're transitioning from an ad group to content starting from its default position, + // project the start position forward as if this were a transition to a new window. Pair defaultPosition = timeline.getPeriodPosition( window, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index cf4643c5da..e9b99acd77 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2; import androidx.annotation.CheckResult; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; @@ -35,8 +34,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** The current {@link Timeline}. */ public final Timeline timeline; - /** The current manifest. */ - public final @Nullable Object manifest; /** The {@link MediaPeriodId} of the currently playing media period in the {@link #timeline}. */ public final MediaPeriodId periodId; /** @@ -48,7 +45,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** * If {@link #periodId} refers to an ad, the position of the suspended content relative to the * start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET} - * if {@link #periodId} does not refer to an ad. + * if {@link #periodId} does not refer to an ad or if the suspended content should be played from + * its default position. */ public final long contentPositionUs; /** The current playback state. One of the {@link Player}.STATE_ constants. */ @@ -91,7 +89,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long startPositionUs, TrackSelectorResult emptyTrackSelectorResult) { return new PlaybackInfo( Timeline.EMPTY, - /* manifest= */ null, DUMMY_MEDIA_PERIOD_ID, startPositionUs, /* contentPositionUs= */ C.TIME_UNSET, @@ -109,7 +106,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * Create playback info. * * @param timeline See {@link #timeline}. - * @param manifest See {@link #manifest}. * @param periodId See {@link #periodId}. * @param startPositionUs See {@link #startPositionUs}. * @param contentPositionUs See {@link #contentPositionUs}. @@ -124,7 +120,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; */ public PlaybackInfo( Timeline timeline, - @Nullable Object manifest, MediaPeriodId periodId, long startPositionUs, long contentPositionUs, @@ -137,7 +132,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long totalBufferedDurationUs, long positionUs) { this.timeline = timeline; - this.manifest = manifest; this.periodId = periodId; this.startPositionUs = startPositionUs; this.contentPositionUs = contentPositionUs; @@ -156,17 +150,26 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * * @param shuffleModeEnabled Whether shuffle mode is enabled. * @param window A writable {@link Timeline.Window}. + * @param period A writable {@link Timeline.Period}. * @return A dummy media period id for the first-to-be-played period of the current timeline. */ public MediaPeriodId getDummyFirstMediaPeriodId( - boolean shuffleModeEnabled, Timeline.Window window) { + boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) { if (timeline.isEmpty()) { return DUMMY_MEDIA_PERIOD_ID; } - int firstPeriodIndex = - timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) - .firstPeriodIndex; - return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex)); + int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex; + int currentPeriodIndex = timeline.getIndexOfPeriod(periodId.periodUid); + long windowSequenceNumber = C.INDEX_UNSET; + if (currentPeriodIndex != C.INDEX_UNSET) { + int currentWindowIndex = timeline.getPeriod(currentPeriodIndex, period).windowIndex; + if (firstWindowIndex == currentWindowIndex) { + // Keep window sequence number if the new position is still in the same window. + windowSequenceNumber = periodId.windowSequenceNumber; + } + } + return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber); } /** @@ -187,7 +190,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long totalBufferedDurationUs) { return new PlaybackInfo( timeline, - manifest, periodId, positionUs, periodId.isAd() ? contentPositionUs : C.TIME_UNSET, @@ -202,17 +204,15 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; } /** - * Copies playback info with new timeline and manifest. + * Copies playback info with the new timeline. * * @param timeline New timeline. See {@link #timeline}. - * @param manifest New manifest. See {@link #manifest}. - * @return Copied playback info with new timeline and manifest. + * @return Copied playback info with the new timeline. */ @CheckResult - public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) { + public PlaybackInfo copyWithTimeline(Timeline timeline) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -236,7 +236,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo copyWithPlaybackState(int playbackState) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -260,7 +259,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo copyWithIsLoading(boolean isLoading) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -286,7 +284,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, @@ -310,7 +307,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo copyWithLoadingMediaPeriodId(MediaPeriodId loadingMediaPeriodId) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 0e19212afa..eed59876f9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -324,6 +324,29 @@ public interface Player { */ interface EventListener { + /** + * Called when the timeline has been refreshed. + * + *

Note that if the timeline has changed then a position discontinuity may also have + * occurred. For example, the current period index may have changed as a result of periods being + * added or removed from the timeline. This will not be reported via a separate call to + * {@link #onPositionDiscontinuity(int)}. + * + * @param timeline The latest timeline. Never null, but may be empty. + * @param reason The {@link TimelineChangeReason} responsible for this timeline change. + */ + @SuppressWarnings("deprecation") + default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + Object manifest = null; + if (timeline.getWindowCount() == 1) { + // Legacy behavior was to report the manifest for single window timelines only. + Timeline.Window window = new Timeline.Window(); + manifest = timeline.getWindow(0, window).manifest; + } + // Call deprecated version. + onTimelineChanged(timeline, manifest, reason); + } + /** * Called when the timeline and/or manifest has been refreshed. * @@ -335,7 +358,11 @@ public interface Player { * @param timeline The latest timeline. Never null, but may be empty. * @param manifest The latest manifest. May be null. * @param reason The {@link TimelineChangeReason} responsible for this timeline change. + * @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be + * accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex, + * window).manifest} for a given window index. */ + @Deprecated default void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {} @@ -396,8 +423,7 @@ public interface Player { * when the source introduces a discontinuity internally). * *

When a position discontinuity occurs as a result of a change to the timeline this method - * is not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this - * case. + * is not called. {@link #onTimelineChanged(Timeline, int)} is called in this case. * * @param reason The {@link DiscontinuityReason} responsible for the discontinuity. */ @@ -428,6 +454,19 @@ public interface Player { @Deprecated abstract class DefaultEventListener implements EventListener { + @Override + @SuppressWarnings("deprecation") + public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + Object manifest = null; + if (timeline.getWindowCount() == 1) { + // Legacy behavior was to report the manifest for single window timelines only. + Timeline.Window window = new Timeline.Window(); + manifest = timeline.getWindow(0, window).manifest; + } + // Call deprecated version. + onTimelineChanged(timeline, manifest, reason); + } + @Override @SuppressWarnings("deprecation") public void onTimelineChanged( @@ -436,7 +475,7 @@ public interface Player { onTimelineChanged(timeline, manifest); } - /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, Object, int)} instead. */ + /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, int)} instead. */ @Deprecated public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { // Do nothing. @@ -524,8 +563,8 @@ public interface Player { int DISCONTINUITY_REASON_INTERNAL = 4; /** - * Reasons for timeline and/or manifest changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, - * {@link #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. + * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link + * #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -722,13 +761,10 @@ public interface Player { /** * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment. - *

- * Playback parameters changes may cause the player to buffer. - * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever - * the currently active playback parameters change. When that listener is called, the parameters - * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch - * may be out of range, in which case they are constrained to a set of permitted values. If it is - * not possible to change the playback parameters, the listener will not be invoked. + * + *

Playback parameters changes may cause the player to buffer. {@link + * EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the + * currently active playback parameters change. * * @param playbackParameters The playback parameters, or {@code null} to use the defaults. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 7904942c1b..49309181a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -55,7 +55,7 @@ public final class PlayerMessage { private final Timeline timeline; private int type; - private @Nullable Object payload; + @Nullable private Object payload; private Handler handler; private int windowIndex; private long positionMs; @@ -134,7 +134,8 @@ public final class PlayerMessage { } /** Returns the message payload forwarded to {@link Target#handleMessage(int, Object)}. */ - public @Nullable Object getPayload() { + @Nullable + public Object getPayload() { return payload; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 056038d97a..8913fbdaba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -144,7 +144,7 @@ public class SimpleExoPlayer extends BasePlayer loadControl, drmSessionManager, bandwidthMeter, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(Clock.DEFAULT), looper); } @@ -156,8 +156,8 @@ public class SimpleExoPlayer extends BasePlayer * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @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. */ @@ -168,7 +168,7 @@ public class SimpleExoPlayer extends BasePlayer LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Looper looper) { this( context, @@ -177,7 +177,7 @@ public class SimpleExoPlayer extends BasePlayer loadControl, drmSessionManager, bandwidthMeter, - analyticsCollectorFactory, + analyticsCollector, Clock.DEFAULT, looper); } @@ -190,8 +190,8 @@ public class SimpleExoPlayer extends BasePlayer * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @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 clock The {@link Clock} that will be used by the instance. Should always be {@link * Clock#DEFAULT}, unless the player is being used from a test. * @param looper The {@link Looper} which must be used for all calls to the player and which is @@ -204,10 +204,11 @@ public class SimpleExoPlayer extends BasePlayer LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Clock clock, Looper looper) { this.bandwidthMeter = bandwidthMeter; + this.analyticsCollector = analyticsCollector; componentListener = new ComponentListener(); videoListeners = new CopyOnWriteArraySet<>(); audioListeners = new CopyOnWriteArraySet<>(); @@ -235,7 +236,7 @@ public class SimpleExoPlayer extends BasePlayer // Build the player and associated objects. player = new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); - analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock); + analyticsCollector.setPlayer(player); addListener(analyticsCollector); addListener(componentListener); videoDebugListeners.add(analyticsCollector); @@ -880,7 +881,8 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public @Nullable ExoPlaybackException getPlaybackError() { + @Nullable + public ExoPlaybackException getPlaybackError() { verifyApplicationThread(); return player.getPlaybackError(); } @@ -1033,26 +1035,12 @@ public class SimpleExoPlayer extends BasePlayer currentCues = Collections.emptyList(); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void sendMessages(ExoPlayerMessage... messages) { - player.sendMessages(messages); - } - @Override public PlayerMessage createMessage(PlayerMessage.Target target) { verifyApplicationThread(); return player.createMessage(target); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void blockingSendMessages(ExoPlayerMessage... messages) { - player.blockingSendMessages(messages); - } - @Override public int getRendererCount() { verifyApplicationThread(); @@ -1083,13 +1071,6 @@ public class SimpleExoPlayer extends BasePlayer return player.getCurrentTimeline(); } - @Override - @Nullable - public Object getCurrentManifest() { - verifyApplicationThread(); - return player.getCurrentManifest(); - } - @Override public int getCurrentPeriodIndex() { verifyApplicationThread(); @@ -1232,8 +1213,7 @@ public class SimpleExoPlayer extends BasePlayer Log.w( TAG, "Player is accessed on the wrong thread. See " - + "https://exoplayer.dev/troubleshooting.html#" - + "what-do-player-is-accessed-on-the-wrong-thread-warnings-mean", + + "https://exoplayer.dev/issues/player-accessed-on-wrong-thread", hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException()); hasNotifiedFullWrongThreadWarning = true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 0c64810d58..32fa3a6e4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -122,6 +122,9 @@ public abstract class Timeline { /** A tag for the window. Not necessarily unique. */ @Nullable public Object tag; + /** The manifest of the window. May be {@code null}. */ + @Nullable public Object manifest; + /** * The start time of the presentation to which this window belongs in milliseconds since the * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes only. @@ -179,6 +182,7 @@ public abstract class Timeline { /** Sets the data held by this window. */ public Window set( @Nullable Object tag, + @Nullable Object manifest, long presentationStartTimeMs, long windowStartTimeMs, boolean isSeekable, @@ -189,6 +193,7 @@ public abstract class Timeline { int lastPeriodIndex, long positionInFirstPeriodUs) { this.tag = tag; + this.manifest = manifest; this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; this.isSeekable = isSeekable; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 094024bc36..825424ae04 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -67,23 +67,6 @@ public class AnalyticsCollector VideoListener, AudioListener { - /** Factory for an analytics collector. */ - public static class Factory { - - /** - * Creates an analytics collector for the specified player. - * - * @param player The {@link Player} for which data will be collected. Can be null, if the player - * is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics - * collector. - * @param clock A {@link Clock} used to generate timestamps. - * @return An analytics collector. - */ - public AnalyticsCollector createAnalyticsCollector(@Nullable Player player, Clock clock) { - return new AnalyticsCollector(player, clock); - } - } - private final CopyOnWriteArraySet listeners; private final Clock clock; private final Window window; @@ -92,17 +75,11 @@ public class AnalyticsCollector private @MonotonicNonNull Player player; /** - * Creates an analytics collector for the specified player. + * Creates an analytics collector. * - * @param player The {@link Player} for which data will be collected. Can be null, if the player - * is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics - * collector. * @param clock A {@link Clock} used to generate timestamps. */ - protected AnalyticsCollector(@Nullable Player player, Clock clock) { - if (player != null) { - this.player = player; - } + public AnalyticsCollector(Clock clock) { this.clock = Assertions.checkNotNull(clock); listeners = new CopyOnWriteArraySet<>(); mediaPeriodQueueTracker = new MediaPeriodQueueTracker(); @@ -437,8 +414,7 @@ public class AnalyticsCollector // having slightly different real times. @Override - public final void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { mediaPeriodQueueTracker.onTimelineChanged(timeline); EventTime eventTime = generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { @@ -687,8 +663,9 @@ public class AnalyticsCollector private final HashMap mediaPeriodIdToInfo; private final Period period; - private @Nullable MediaPeriodInfo lastReportedPlayingMediaPeriod; - private @Nullable MediaPeriodInfo readingMediaPeriod; + @Nullable private MediaPeriodInfo lastPlayingMediaPeriod; + @Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod; + @Nullable private MediaPeriodInfo readingMediaPeriod; private Timeline timeline; private boolean isSeeking; @@ -706,7 +683,8 @@ public class AnalyticsCollector * always return null to reflect the uncertainty about the current playing period. May also be * null, if the timeline is empty or no media period is active yet. */ - public @Nullable MediaPeriodInfo getPlayingMediaPeriod() { + @Nullable + public MediaPeriodInfo getPlayingMediaPeriod() { return mediaPeriodInfoQueue.isEmpty() || timeline.isEmpty() || isSeeking ? null : mediaPeriodInfoQueue.get(0); @@ -719,7 +697,8 @@ public class AnalyticsCollector * reported until the seek or preparation is processed. May be null, if no media period is * active yet. */ - public @Nullable MediaPeriodInfo getLastReportedPlayingMediaPeriod() { + @Nullable + public MediaPeriodInfo getLastReportedPlayingMediaPeriod() { return lastReportedPlayingMediaPeriod; } @@ -727,7 +706,8 @@ public class AnalyticsCollector * Returns the {@link MediaPeriodInfo} of the media period currently being read by the player. * May be null, if the player is not reading a media period. */ - public @Nullable MediaPeriodInfo getReadingMediaPeriod() { + @Nullable + public MediaPeriodInfo getReadingMediaPeriod() { return readingMediaPeriod; } @@ -736,14 +716,16 @@ public class AnalyticsCollector * currently loading or will be the next one loading. May be null, if no media period is active * yet. */ - public @Nullable MediaPeriodInfo getLoadingMediaPeriod() { + @Nullable + public MediaPeriodInfo getLoadingMediaPeriod() { return mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(mediaPeriodInfoQueue.size() - 1); } /** Returns the {@link MediaPeriodInfo} for the given {@link MediaPeriodId}. */ - public @Nullable MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) { + @Nullable + public MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) { return mediaPeriodIdToInfo.get(mediaPeriodId); } @@ -756,7 +738,8 @@ public class AnalyticsCollector * Tries to find an existing media period info from the specified window index. Only returns a * non-null media period info if there is a unique, unambiguous match. */ - public @Nullable MediaPeriodInfo tryResolveWindowIndex(int windowIndex) { + @Nullable + public MediaPeriodInfo tryResolveWindowIndex(int windowIndex) { MediaPeriodInfo match = null; for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { MediaPeriodInfo info = mediaPeriodInfoQueue.get(i); @@ -775,7 +758,7 @@ public class AnalyticsCollector /** Updates the queue with a reported position discontinuity . */ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } /** Updates the queue with a reported timeline change. */ @@ -790,7 +773,7 @@ public class AnalyticsCollector readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline); } this.timeline = timeline; - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } /** Updates the queue with a reported start of seek. */ @@ -801,7 +784,7 @@ public class AnalyticsCollector /** Updates the queue with a reported processed seek. */ public void onSeekProcessed() { isSeeking = false; - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } /** Updates the queue with a newly created media period. */ @@ -811,8 +794,9 @@ public class AnalyticsCollector new MediaPeriodInfo(mediaPeriodId, isInTimeline ? timeline : Timeline.EMPTY, windowIndex); mediaPeriodInfoQueue.add(mediaPeriodInfo); mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo); + lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); if (mediaPeriodInfoQueue.size() == 1 && !timeline.isEmpty()) { - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } } @@ -830,6 +814,9 @@ public class AnalyticsCollector if (readingMediaPeriod != null && mediaPeriodId.equals(readingMediaPeriod.mediaPeriodId)) { readingMediaPeriod = mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(0); } + if (!mediaPeriodInfoQueue.isEmpty()) { + lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); + } return true; } @@ -838,12 +825,6 @@ public class AnalyticsCollector readingMediaPeriod = mediaPeriodIdToInfo.get(mediaPeriodId); } - private void updateLastReportedPlayingMediaPeriod() { - if (!mediaPeriodInfoQueue.isEmpty()) { - lastReportedPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); - } - } - private MediaPeriodInfo updateMediaPeriodInfoToNewTimeline( MediaPeriodInfo info, Timeline newTimeline) { int newPeriodIndex = newTimeline.getIndexOfPeriod(info.mediaPeriodId.periodUid); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 48578d8853..be62ad99d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -68,7 +68,7 @@ public interface AnalyticsListener { * Media period identifier for the media period this event belongs to, or {@code null} if the * event is not associated with a specific media period. */ - public final @Nullable MediaPeriodId mediaPeriodId; + @Nullable public final MediaPeriodId mediaPeriodId; /** * Position in the window or ad this event belongs to at the time of the event, in milliseconds. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java index 4ac7ad6506..183a74544d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java @@ -46,7 +46,7 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag private final Timeline.Period period; private final HashMap sessions; - @MonotonicNonNull private Listener listener; + private @MonotonicNonNull Listener listener; private Timeline currentTimeline; @Nullable private MediaPeriodId currentMediaPeriodId; @Nullable private String activeSessionId; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java new file mode 100644 index 0000000000..ed127bc550 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java @@ -0,0 +1,969 @@ +/* + * 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.analytics; + +import android.os.SystemClock; +import androidx.annotation.IntDef; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** Statistics about playbacks. */ +public final class PlaybackStats { + + /** + * State of a playback. One of {@link #PLAYBACK_STATE_NOT_STARTED}, {@link + * #PLAYBACK_STATE_JOINING_FOREGROUND}, {@link #PLAYBACK_STATE_JOINING_BACKGROUND}, {@link + * #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_SEEKING}, + * {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_PAUSED_BUFFERING}, {@link + * #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link + * #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED} or {@link #PLAYBACK_STATE_SUSPENDED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) + @IntDef({ + PLAYBACK_STATE_NOT_STARTED, + PLAYBACK_STATE_JOINING_BACKGROUND, + PLAYBACK_STATE_JOINING_FOREGROUND, + PLAYBACK_STATE_PLAYING, + PLAYBACK_STATE_PAUSED, + PLAYBACK_STATE_SEEKING, + PLAYBACK_STATE_BUFFERING, + PLAYBACK_STATE_PAUSED_BUFFERING, + PLAYBACK_STATE_SEEK_BUFFERING, + PLAYBACK_STATE_ENDED, + PLAYBACK_STATE_STOPPED, + PLAYBACK_STATE_FAILED, + PLAYBACK_STATE_SUSPENDED + }) + @interface PlaybackState {} + /** Playback has not started (initial state). */ + public static final int PLAYBACK_STATE_NOT_STARTED = 0; + /** Playback is buffering in the background for initial playback start. */ + public static final int PLAYBACK_STATE_JOINING_BACKGROUND = 1; + /** Playback is buffering in the foreground for initial playback start. */ + public static final int PLAYBACK_STATE_JOINING_FOREGROUND = 2; + /** Playback is actively playing. */ + public static final int PLAYBACK_STATE_PLAYING = 3; + /** Playback is paused but ready to play. */ + public static final int PLAYBACK_STATE_PAUSED = 4; + /** Playback is handling a seek. */ + public static final int PLAYBACK_STATE_SEEKING = 5; + /** Playback is buffering to restart playback. */ + public static final int PLAYBACK_STATE_BUFFERING = 6; + /** Playback is buffering while paused. */ + public static final int PLAYBACK_STATE_PAUSED_BUFFERING = 7; + /** Playback is buffering after a seek. */ + public static final int PLAYBACK_STATE_SEEK_BUFFERING = 8; + /** Playback has reached the end of the media. */ + public static final int PLAYBACK_STATE_ENDED = 9; + /** Playback is stopped and can be resumed. */ + public static final int PLAYBACK_STATE_STOPPED = 10; + /** Playback is stopped due a fatal error and can be retried. */ + public static final int PLAYBACK_STATE_FAILED = 11; + /** Playback is suspended, e.g. because the user left or it is interrupted by another playback. */ + public static final int PLAYBACK_STATE_SUSPENDED = 12; + /** Total number of playback states. */ + /* package */ static final int PLAYBACK_STATE_COUNT = 13; + + /** Empty playback stats. */ + public static final PlaybackStats EMPTY = merge(/* nothing */ ); + + /** + * Returns the combined {@link PlaybackStats} for all input {@link PlaybackStats}. + * + *

Note that the full history of events is not kept as the history only makes sense in the + * context of a single playback. + * + * @param playbackStats Array of {@link PlaybackStats} to combine. + * @return The combined {@link PlaybackStats}. + */ + public static PlaybackStats merge(PlaybackStats... playbackStats) { + int playbackCount = 0; + long[] playbackStateDurationsMs = new long[PLAYBACK_STATE_COUNT]; + long firstReportedTimeMs = C.TIME_UNSET; + int foregroundPlaybackCount = 0; + int abandonedBeforeReadyCount = 0; + int endedCount = 0; + int backgroundJoiningCount = 0; + long totalValidJoinTimeMs = C.TIME_UNSET; + int validJoinTimeCount = 0; + int totalPauseCount = 0; + int totalPauseBufferCount = 0; + int totalSeekCount = 0; + int totalRebufferCount = 0; + long maxRebufferTimeMs = C.TIME_UNSET; + int adPlaybackCount = 0; + long totalVideoFormatHeightTimeMs = 0; + long totalVideoFormatHeightTimeProduct = 0; + long totalVideoFormatBitrateTimeMs = 0; + long totalVideoFormatBitrateTimeProduct = 0; + long totalAudioFormatTimeMs = 0; + long totalAudioFormatBitrateTimeProduct = 0; + int initialVideoFormatHeightCount = 0; + int initialVideoFormatBitrateCount = 0; + int totalInitialVideoFormatHeight = C.LENGTH_UNSET; + long totalInitialVideoFormatBitrate = C.LENGTH_UNSET; + int initialAudioFormatBitrateCount = 0; + long totalInitialAudioFormatBitrate = C.LENGTH_UNSET; + long totalBandwidthTimeMs = 0; + long totalBandwidthBytes = 0; + long totalDroppedFrames = 0; + long totalAudioUnderruns = 0; + int fatalErrorPlaybackCount = 0; + int fatalErrorCount = 0; + int nonFatalErrorCount = 0; + for (PlaybackStats stats : playbackStats) { + playbackCount += stats.playbackCount; + for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) { + playbackStateDurationsMs[i] += stats.playbackStateDurationsMs[i]; + } + if (firstReportedTimeMs == C.TIME_UNSET) { + firstReportedTimeMs = stats.firstReportedTimeMs; + } else if (stats.firstReportedTimeMs != C.TIME_UNSET) { + firstReportedTimeMs = Math.min(firstReportedTimeMs, stats.firstReportedTimeMs); + } + foregroundPlaybackCount += stats.foregroundPlaybackCount; + abandonedBeforeReadyCount += stats.abandonedBeforeReadyCount; + endedCount += stats.endedCount; + backgroundJoiningCount += stats.backgroundJoiningCount; + if (totalValidJoinTimeMs == C.TIME_UNSET) { + totalValidJoinTimeMs = stats.totalValidJoinTimeMs; + } else if (stats.totalValidJoinTimeMs != C.TIME_UNSET) { + totalValidJoinTimeMs += stats.totalValidJoinTimeMs; + } + validJoinTimeCount += stats.validJoinTimeCount; + totalPauseCount += stats.totalPauseCount; + totalPauseBufferCount += stats.totalPauseBufferCount; + totalSeekCount += stats.totalSeekCount; + totalRebufferCount += stats.totalRebufferCount; + if (maxRebufferTimeMs == C.TIME_UNSET) { + maxRebufferTimeMs = stats.maxRebufferTimeMs; + } else if (stats.maxRebufferTimeMs != C.TIME_UNSET) { + maxRebufferTimeMs = Math.max(maxRebufferTimeMs, stats.maxRebufferTimeMs); + } + adPlaybackCount += stats.adPlaybackCount; + totalVideoFormatHeightTimeMs += stats.totalVideoFormatHeightTimeMs; + totalVideoFormatHeightTimeProduct += stats.totalVideoFormatHeightTimeProduct; + totalVideoFormatBitrateTimeMs += stats.totalVideoFormatBitrateTimeMs; + totalVideoFormatBitrateTimeProduct += stats.totalVideoFormatBitrateTimeProduct; + totalAudioFormatTimeMs += stats.totalAudioFormatTimeMs; + totalAudioFormatBitrateTimeProduct += stats.totalAudioFormatBitrateTimeProduct; + initialVideoFormatHeightCount += stats.initialVideoFormatHeightCount; + initialVideoFormatBitrateCount += stats.initialVideoFormatBitrateCount; + if (totalInitialVideoFormatHeight == C.LENGTH_UNSET) { + totalInitialVideoFormatHeight = stats.totalInitialVideoFormatHeight; + } else if (stats.totalInitialVideoFormatHeight != C.LENGTH_UNSET) { + totalInitialVideoFormatHeight += stats.totalInitialVideoFormatHeight; + } + if (totalInitialVideoFormatBitrate == C.LENGTH_UNSET) { + totalInitialVideoFormatBitrate = stats.totalInitialVideoFormatBitrate; + } else if (stats.totalInitialVideoFormatBitrate != C.LENGTH_UNSET) { + totalInitialVideoFormatBitrate += stats.totalInitialVideoFormatBitrate; + } + initialAudioFormatBitrateCount += stats.initialAudioFormatBitrateCount; + if (totalInitialAudioFormatBitrate == C.LENGTH_UNSET) { + totalInitialAudioFormatBitrate = stats.totalInitialAudioFormatBitrate; + } else if (stats.totalInitialAudioFormatBitrate != C.LENGTH_UNSET) { + totalInitialAudioFormatBitrate += stats.totalInitialAudioFormatBitrate; + } + totalBandwidthTimeMs += stats.totalBandwidthTimeMs; + totalBandwidthBytes += stats.totalBandwidthBytes; + totalDroppedFrames += stats.totalDroppedFrames; + totalAudioUnderruns += stats.totalAudioUnderruns; + fatalErrorPlaybackCount += stats.fatalErrorPlaybackCount; + fatalErrorCount += stats.fatalErrorCount; + nonFatalErrorCount += stats.nonFatalErrorCount; + } + return new PlaybackStats( + playbackCount, + playbackStateDurationsMs, + /* playbackStateHistory */ Collections.emptyList(), + /* mediaTimeHistory= */ Collections.emptyList(), + firstReportedTimeMs, + foregroundPlaybackCount, + abandonedBeforeReadyCount, + endedCount, + backgroundJoiningCount, + totalValidJoinTimeMs, + validJoinTimeCount, + totalPauseCount, + totalPauseBufferCount, + totalSeekCount, + totalRebufferCount, + maxRebufferTimeMs, + adPlaybackCount, + /* videoFormatHistory= */ Collections.emptyList(), + /* audioFormatHistory= */ Collections.emptyList(), + totalVideoFormatHeightTimeMs, + totalVideoFormatHeightTimeProduct, + totalVideoFormatBitrateTimeMs, + totalVideoFormatBitrateTimeProduct, + totalAudioFormatTimeMs, + totalAudioFormatBitrateTimeProduct, + initialVideoFormatHeightCount, + initialVideoFormatBitrateCount, + totalInitialVideoFormatHeight, + totalInitialVideoFormatBitrate, + initialAudioFormatBitrateCount, + totalInitialAudioFormatBitrate, + totalBandwidthTimeMs, + totalBandwidthBytes, + totalDroppedFrames, + totalAudioUnderruns, + fatalErrorPlaybackCount, + fatalErrorCount, + nonFatalErrorCount, + /* fatalErrorHistory= */ Collections.emptyList(), + /* nonFatalErrorHistory= */ Collections.emptyList()); + } + + /** The number of individual playbacks for which these stats were collected. */ + public final int playbackCount; + + // Playback state stats. + + /** + * The playback state history as ordered pairs of the {@link EventTime} at which a state became + * active and the {@link PlaybackState}. + */ + public final List> playbackStateHistory; + /** + * The media time history as an ordered list of long[2] arrays with [0] being the realtime as + * returned by {@code SystemClock.elapsedRealtime()} and [1] being the media time at this + * realtime, in milliseconds. + */ + public final List mediaTimeHistory; + /** + * The elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} of the first + * reported playback event, or {@link C#TIME_UNSET} if no event has been reported. + */ + public final long firstReportedTimeMs; + /** The number of playbacks which were the active foreground playback at some point. */ + public final int foregroundPlaybackCount; + /** The number of playbacks which were abandoned before they were ready to play. */ + public final int abandonedBeforeReadyCount; + /** The number of playbacks which reached the ended state at least once. */ + public final int endedCount; + /** The number of playbacks which were pre-buffered in the background. */ + public final int backgroundJoiningCount; + /** + * The total time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if no valid + * join time could be determined. + * + *

Note that this does not include background joining time. A join time may be invalid if the + * playback never reached {@link #PLAYBACK_STATE_PLAYING} or {@link #PLAYBACK_STATE_PAUSED}, or + * joining was interrupted by a seek, stop, or error state. + */ + public final long totalValidJoinTimeMs; + /** + * The number of playbacks with a valid join time as documented in {@link #totalValidJoinTimeMs}. + */ + public final int validJoinTimeCount; + /** The total number of times a playback has been paused. */ + public final int totalPauseCount; + /** The total number of times a playback has been paused while rebuffering. */ + public final int totalPauseBufferCount; + /** + * The total number of times a seek occurred. This includes seeks happening before playback + * resumed after another seek. + */ + public final int totalSeekCount; + /** + * The total number of times a rebuffer occurred. This excludes initial joining and buffering + * after seek. + */ + public final int totalRebufferCount; + /** + * The maximum time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET} if no + * rebuffer occurred. + */ + public final long maxRebufferTimeMs; + /** The number of ad playbacks. */ + public final int adPlaybackCount; + + // Format stats. + + /** + * The video format history as ordered pairs of the {@link EventTime} at which a format started + * being used and the {@link Format}. The {@link Format} may be null if no video format was used. + */ + public final List> videoFormatHistory; + /** + * The audio format history as ordered pairs of the {@link EventTime} at which a format started + * being used and the {@link Format}. The {@link Format} may be null if no audio format was used. + */ + public final List> audioFormatHistory; + /** The total media time for which video format height data is available, in milliseconds. */ + public final long totalVideoFormatHeightTimeMs; + /** + * The accumulated sum of all video format heights, in pixels, times the time the format was used + * for playback, in milliseconds. + */ + public final long totalVideoFormatHeightTimeProduct; + /** The total media time for which video format bitrate data is available, in milliseconds. */ + public final long totalVideoFormatBitrateTimeMs; + /** + * The accumulated sum of all video format bitrates, in bits per second, times the time the format + * was used for playback, in milliseconds. + */ + public final long totalVideoFormatBitrateTimeProduct; + /** The total media time for which audio format data is available, in milliseconds. */ + public final long totalAudioFormatTimeMs; + /** + * The accumulated sum of all audio format bitrates, in bits per second, times the time the format + * was used for playback, in milliseconds. + */ + public final long totalAudioFormatBitrateTimeProduct; + /** The number of playbacks with initial video format height data. */ + public final int initialVideoFormatHeightCount; + /** The number of playbacks with initial video format bitrate data. */ + public final int initialVideoFormatBitrateCount; + /** + * The total initial video format height for all playbacks, in pixels, or {@link C#LENGTH_UNSET} + * if no initial video format data is available. + */ + public final int totalInitialVideoFormatHeight; + /** + * The total initial video format bitrate for all playbacks, in bits per second, or {@link + * C#LENGTH_UNSET} if no initial video format data is available. + */ + public final long totalInitialVideoFormatBitrate; + /** The number of playbacks with initial audio format bitrate data. */ + public final int initialAudioFormatBitrateCount; + /** + * The total initial audio format bitrate for all playbacks, in bits per second, or {@link + * C#LENGTH_UNSET} if no initial audio format data is available. + */ + public final long totalInitialAudioFormatBitrate; + + // Bandwidth stats. + + /** The total time for which bandwidth measurement data is available, in milliseconds. */ + public final long totalBandwidthTimeMs; + /** The total bytes transferred during {@link #totalBandwidthTimeMs}. */ + public final long totalBandwidthBytes; + + // Renderer quality stats. + + /** The total number of dropped video frames. */ + public final long totalDroppedFrames; + /** The total number of audio underruns. */ + public final long totalAudioUnderruns; + + // Error stats. + + /** + * The total number of playback with at least one fatal error. Errors are fatal if playback + * stopped due to this error. + */ + public final int fatalErrorPlaybackCount; + /** The total number of fatal errors. Errors are fatal if playback stopped due to this error. */ + public final int fatalErrorCount; + /** + * The total number of non-fatal errors. Error are non-fatal if playback can recover from the + * error without stopping. + */ + public final int nonFatalErrorCount; + /** + * The history of fatal errors as ordered pairs of the {@link EventTime} at which an error + * occurred and the error. Errors are fatal if playback stopped due to this error. + */ + public final List> fatalErrorHistory; + /** + * The history of non-fatal errors as ordered pairs of the {@link EventTime} at which an error + * occurred and the error. Error are non-fatal if playback can recover from the error without + * stopping. + */ + public final List> nonFatalErrorHistory; + + private final long[] playbackStateDurationsMs; + + /* package */ PlaybackStats( + int playbackCount, + long[] playbackStateDurationsMs, + List> playbackStateHistory, + List mediaTimeHistory, + long firstReportedTimeMs, + int foregroundPlaybackCount, + int abandonedBeforeReadyCount, + int endedCount, + int backgroundJoiningCount, + long totalValidJoinTimeMs, + int validJoinTimeCount, + int totalPauseCount, + int totalPauseBufferCount, + int totalSeekCount, + int totalRebufferCount, + long maxRebufferTimeMs, + int adPlaybackCount, + List> videoFormatHistory, + List> audioFormatHistory, + long totalVideoFormatHeightTimeMs, + long totalVideoFormatHeightTimeProduct, + long totalVideoFormatBitrateTimeMs, + long totalVideoFormatBitrateTimeProduct, + long totalAudioFormatTimeMs, + long totalAudioFormatBitrateTimeProduct, + int initialVideoFormatHeightCount, + int initialVideoFormatBitrateCount, + int totalInitialVideoFormatHeight, + long totalInitialVideoFormatBitrate, + int initialAudioFormatBitrateCount, + long totalInitialAudioFormatBitrate, + long totalBandwidthTimeMs, + long totalBandwidthBytes, + long totalDroppedFrames, + long totalAudioUnderruns, + int fatalErrorPlaybackCount, + int fatalErrorCount, + int nonFatalErrorCount, + List> fatalErrorHistory, + List> nonFatalErrorHistory) { + this.playbackCount = playbackCount; + this.playbackStateDurationsMs = playbackStateDurationsMs; + this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory); + this.mediaTimeHistory = Collections.unmodifiableList(mediaTimeHistory); + this.firstReportedTimeMs = firstReportedTimeMs; + this.foregroundPlaybackCount = foregroundPlaybackCount; + this.abandonedBeforeReadyCount = abandonedBeforeReadyCount; + this.endedCount = endedCount; + this.backgroundJoiningCount = backgroundJoiningCount; + this.totalValidJoinTimeMs = totalValidJoinTimeMs; + this.validJoinTimeCount = validJoinTimeCount; + this.totalPauseCount = totalPauseCount; + this.totalPauseBufferCount = totalPauseBufferCount; + this.totalSeekCount = totalSeekCount; + this.totalRebufferCount = totalRebufferCount; + this.maxRebufferTimeMs = maxRebufferTimeMs; + this.adPlaybackCount = adPlaybackCount; + this.videoFormatHistory = Collections.unmodifiableList(videoFormatHistory); + this.audioFormatHistory = Collections.unmodifiableList(audioFormatHistory); + this.totalVideoFormatHeightTimeMs = totalVideoFormatHeightTimeMs; + this.totalVideoFormatHeightTimeProduct = totalVideoFormatHeightTimeProduct; + this.totalVideoFormatBitrateTimeMs = totalVideoFormatBitrateTimeMs; + this.totalVideoFormatBitrateTimeProduct = totalVideoFormatBitrateTimeProduct; + this.totalAudioFormatTimeMs = totalAudioFormatTimeMs; + this.totalAudioFormatBitrateTimeProduct = totalAudioFormatBitrateTimeProduct; + this.initialVideoFormatHeightCount = initialVideoFormatHeightCount; + this.initialVideoFormatBitrateCount = initialVideoFormatBitrateCount; + this.totalInitialVideoFormatHeight = totalInitialVideoFormatHeight; + this.totalInitialVideoFormatBitrate = totalInitialVideoFormatBitrate; + this.initialAudioFormatBitrateCount = initialAudioFormatBitrateCount; + this.totalInitialAudioFormatBitrate = totalInitialAudioFormatBitrate; + this.totalBandwidthTimeMs = totalBandwidthTimeMs; + this.totalBandwidthBytes = totalBandwidthBytes; + this.totalDroppedFrames = totalDroppedFrames; + this.totalAudioUnderruns = totalAudioUnderruns; + this.fatalErrorPlaybackCount = fatalErrorPlaybackCount; + this.fatalErrorCount = fatalErrorCount; + this.nonFatalErrorCount = nonFatalErrorCount; + this.fatalErrorHistory = Collections.unmodifiableList(fatalErrorHistory); + this.nonFatalErrorHistory = Collections.unmodifiableList(nonFatalErrorHistory); + } + + /** + * Returns the total time spent in a given {@link PlaybackState}, in milliseconds. + * + * @param playbackState A {@link PlaybackState}. + * @return Total spent in the given playback state, in milliseconds + */ + public long getPlaybackStateDurationMs(@PlaybackState int playbackState) { + return playbackStateDurationsMs[playbackState]; + } + + /** + * Returns the {@link PlaybackState} at the given time. + * + * @param realtimeMs The time as returned by {@link SystemClock#elapsedRealtime()}. + * @return The {@link PlaybackState} at that time, or {@link #PLAYBACK_STATE_NOT_STARTED} if the + * given time is before the first known playback state in the history. + */ + public @PlaybackState int getPlaybackStateAtTime(long realtimeMs) { + @PlaybackState int state = PLAYBACK_STATE_NOT_STARTED; + for (Pair timeAndState : playbackStateHistory) { + if (timeAndState.first.realtimeMs > realtimeMs) { + break; + } + state = timeAndState.second; + } + return state; + } + + /** + * Returns the estimated media time at the given realtime, in milliseconds, or {@link + * C#TIME_UNSET} if the media time history is unknown. + * + * @param realtimeMs The realtime as returned by {@link SystemClock#elapsedRealtime()}. + * @return The estimated media time in milliseconds at this realtime, {@link C#TIME_UNSET} if no + * estimate can be given. + */ + public long getMediaTimeMsAtRealtimeMs(long realtimeMs) { + if (mediaTimeHistory.isEmpty()) { + return C.TIME_UNSET; + } + int nextIndex = 0; + while (nextIndex < mediaTimeHistory.size() + && mediaTimeHistory.get(nextIndex)[0] <= realtimeMs) { + nextIndex++; + } + if (nextIndex == 0) { + return mediaTimeHistory.get(0)[1]; + } + if (nextIndex == mediaTimeHistory.size()) { + return mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1]; + } + long prevRealtimeMs = mediaTimeHistory.get(nextIndex - 1)[0]; + long prevMediaTimeMs = mediaTimeHistory.get(nextIndex - 1)[1]; + long nextRealtimeMs = mediaTimeHistory.get(nextIndex)[0]; + long nextMediaTimeMs = mediaTimeHistory.get(nextIndex)[1]; + long realtimeDurationMs = nextRealtimeMs - prevRealtimeMs; + if (realtimeDurationMs == 0) { + return prevMediaTimeMs; + } + float fraction = (float) (realtimeMs - prevRealtimeMs) / realtimeDurationMs; + return prevMediaTimeMs + (long) ((nextMediaTimeMs - prevMediaTimeMs) * fraction); + } + + /** + * Returns the mean time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if + * no valid join time is available. Only includes playbacks with valid join times as documented in + * {@link #totalValidJoinTimeMs}. + */ + public long getMeanJoinTimeMs() { + return validJoinTimeCount == 0 ? C.TIME_UNSET : totalValidJoinTimeMs / validJoinTimeCount; + } + + /** + * Returns the total time spent joining the playback in foreground, in milliseconds. This does + * include invalid join times where the playback never reached {@link #PLAYBACK_STATE_PLAYING} or + * {@link #PLAYBACK_STATE_PAUSED}, or joining was interrupted by a seek, stop, or error state. + */ + public long getTotalJoinTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND); + } + + /** Returns the total time spent actively playing, in milliseconds. */ + public long getTotalPlayTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_PLAYING); + } + + /** + * Returns the mean time spent actively playing per foreground playback, in milliseconds, or + * {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPlayTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPlayTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time spent in a paused state, in milliseconds. */ + public long getTotalPausedTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED) + + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING); + } + + /** + * Returns the mean time spent in a paused state per foreground playback, in milliseconds, or + * {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPausedTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPausedTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the total time spent rebuffering, in milliseconds. This excludes initial join times, + * buffer times after a seek and buffering while paused. + */ + public long getTotalRebufferTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING); + } + + /** + * Returns the mean time spent rebuffering per foreground playback, in milliseconds, or {@link + * C#TIME_UNSET} if no playback has been in foreground. This excludes initial join times, buffer + * times after a seek and buffering while paused. + */ + public long getMeanRebufferTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalRebufferTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the mean time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET} + * if no rebuffer was recorded. This excludes initial join times and buffer times after a seek. + */ + public long getMeanSingleRebufferTimeMs() { + return totalRebufferCount == 0 + ? C.TIME_UNSET + : (getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING)) + / totalRebufferCount; + } + + /** + * Returns the total time spent from the start of a seek until playback is ready again, in + * milliseconds. + */ + public long getTotalSeekTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING); + } + + /** + * Returns the mean time spent per foreground playback from the start of a seek until playback is + * ready again, in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanSeekTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalSeekTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the mean time spent from the start of a single seek until playback is ready again, in + * milliseconds, or {@link C#TIME_UNSET} if no seek occurred. + */ + public long getMeanSingleSeekTimeMs() { + return totalSeekCount == 0 ? C.TIME_UNSET : getTotalSeekTimeMs() / totalSeekCount; + } + + /** + * Returns the total time spent actively waiting for playback, in milliseconds. This includes all + * join times, rebuffer times and seek times, but excludes times without user intention to play, + * e.g. all paused states. + */ + public long getTotalWaitTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND) + + getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING); + } + + /** + * Returns the mean time spent actively waiting for playback per foreground playback, in + * milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. This includes all + * join times, rebuffer times and seek times, but excludes times without user intention to play, + * e.g. all paused states. + */ + public long getMeanWaitTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalWaitTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time spent playing or actively waiting for playback, in milliseconds. */ + public long getTotalPlayAndWaitTimeMs() { + return getTotalPlayTimeMs() + getTotalWaitTimeMs(); + } + + /** + * Returns the mean time spent playing or actively waiting for playback per foreground playback, + * in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPlayAndWaitTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPlayAndWaitTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time covered by any playback state, in milliseconds. */ + public long getTotalElapsedTimeMs() { + long totalTimeMs = 0; + for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) { + totalTimeMs += playbackStateDurationsMs[i]; + } + return totalTimeMs; + } + + /** + * Returns the mean time covered by any playback state per playback, in milliseconds, or {@link + * C#TIME_UNSET} if no playback was recorded. + */ + public long getMeanElapsedTimeMs() { + return playbackCount == 0 ? C.TIME_UNSET : getTotalElapsedTimeMs() / playbackCount; + } + + /** + * Returns the ratio of foreground playbacks which were abandoned before they were ready to play, + * or {@code 0.0} if no playback has been in foreground. + */ + public float getAbandonedBeforeReadyRatio() { + int foregroundAbandonedBeforeReady = + abandonedBeforeReadyCount - (playbackCount - foregroundPlaybackCount); + return foregroundPlaybackCount == 0 + ? 0f + : (float) foregroundAbandonedBeforeReady / foregroundPlaybackCount; + } + + /** + * Returns the ratio of foreground playbacks which reached the ended state at least once, or + * {@code 0.0} if no playback has been in foreground. + */ + public float getEndedRatio() { + return foregroundPlaybackCount == 0 ? 0f : (float) endedCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a playback has been paused per foreground playback, or {@code + * 0.0} if no playback has been in foreground. + */ + public float getMeanPauseCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalPauseCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a playback has been paused while rebuffering per foreground + * playback, or {@code 0.0} if no playback has been in foreground. + */ + public float getMeanPauseBufferCount() { + return foregroundPlaybackCount == 0 + ? 0f + : (float) totalPauseBufferCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a seek occurred per foreground playback, or {@code 0.0} if no + * playback has been in foreground. This includes seeks happening before playback resumed after + * another seek. + */ + public float getMeanSeekCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalSeekCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a rebuffer occurred per foreground playback, or {@code 0.0} if + * no playback has been in foreground. This excludes initial joining and buffering after seek. + */ + public float getMeanRebufferCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalRebufferCount / foregroundPlaybackCount; + } + + /** + * Returns the ratio of wait times to the total time spent playing and waiting, or {@code 0.0} if + * no time was spend playing or waiting. This is equivalent to {@link #getTotalWaitTimeMs()} / + * {@link #getTotalPlayAndWaitTimeMs()} and also to {@link #getJoinTimeRatio()} + {@link + * #getRebufferTimeRatio()} + {@link #getSeekTimeRatio()}. + */ + public float getWaitTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalWaitTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of foreground join time to the total time spent playing and waiting, or + * {@code 0.0} if no time was spend playing or waiting. This is equivalent to {@link + * #getTotalJoinTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getJoinTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalJoinTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of rebuffer time to the total time spent playing and waiting, or {@code 0.0} + * if no time was spend playing or waiting. This is equivalent to {@link + * #getTotalRebufferTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getRebufferTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalRebufferTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of seek time to the total time spent playing and waiting, or {@code 0.0} if + * no time was spend playing or waiting. This is equivalent to {@link #getTotalSeekTimeMs()} / + * {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getSeekTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalSeekTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the rate of rebuffer events, in rebuffers per play time second, or {@code 0.0} if no + * time was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenRebuffers()}. + */ + public float getRebufferRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalRebufferCount / playTimeMs; + } + + /** + * Returns the mean play time between rebuffer events, in seconds. This is equivalent to 1.0 / + * {@link #getRebufferRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenRebuffers() { + return 1f / getRebufferRate(); + } + + /** + * Returns the mean initial video format height, in pixels, or {@link C#LENGTH_UNSET} if no video + * format data is available. + */ + public int getMeanInitialVideoFormatHeight() { + return initialVideoFormatHeightCount == 0 + ? C.LENGTH_UNSET + : totalInitialVideoFormatHeight / initialVideoFormatHeightCount; + } + + /** + * Returns the mean initial video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if + * no video format data is available. + */ + public int getMeanInitialVideoFormatBitrate() { + return initialVideoFormatBitrateCount == 0 + ? C.LENGTH_UNSET + : (int) (totalInitialVideoFormatBitrate / initialVideoFormatBitrateCount); + } + + /** + * Returns the mean initial audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if + * no audio format data is available. + */ + public int getMeanInitialAudioFormatBitrate() { + return initialAudioFormatBitrateCount == 0 + ? C.LENGTH_UNSET + : (int) (totalInitialAudioFormatBitrate / initialAudioFormatBitrateCount); + } + + /** + * Returns the mean video format height, in pixels, or {@link C#LENGTH_UNSET} if no video format + * data is available. This is a weighted average taking the time the format was used for playback + * into account. + */ + public int getMeanVideoFormatHeight() { + return totalVideoFormatHeightTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalVideoFormatHeightTimeProduct / totalVideoFormatHeightTimeMs); + } + + /** + * Returns the mean video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no + * video format data is available. This is a weighted average taking the time the format was used + * for playback into account. + */ + public int getMeanVideoFormatBitrate() { + return totalVideoFormatBitrateTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalVideoFormatBitrateTimeProduct / totalVideoFormatBitrateTimeMs); + } + + /** + * Returns the mean audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no + * audio format data is available. This is a weighted average taking the time the format was used + * for playback into account. + */ + public int getMeanAudioFormatBitrate() { + return totalAudioFormatTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalAudioFormatBitrateTimeProduct / totalAudioFormatTimeMs); + } + + /** + * Returns the mean network bandwidth based on transfer measurements, in bits per second, or + * {@link C#LENGTH_UNSET} if no transfer data is available. + */ + public int getMeanBandwidth() { + return totalBandwidthTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalBandwidthBytes * 8000 / totalBandwidthTimeMs); + } + + /** + * Returns the mean rate at which video frames are dropped, in dropped frames per play time + * second, or {@code 0.0} if no time was spent playing. + */ + public float getDroppedFramesRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalDroppedFrames / playTimeMs; + } + + /** + * Returns the mean rate at which audio underruns occurred, in underruns per play time second, or + * {@code 0.0} if no time was spent playing. + */ + public float getAudioUnderrunRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalAudioUnderruns / playTimeMs; + } + + /** + * Returns the ratio of foreground playbacks which experienced fatal errors, or {@code 0.0} if no + * playback has been in foreground. + */ + public float getFatalErrorRatio() { + return foregroundPlaybackCount == 0 + ? 0f + : (float) fatalErrorPlaybackCount / foregroundPlaybackCount; + } + + /** + * Returns the rate of fatal errors, in errors per play time second, or {@code 0.0} if no time was + * spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenFatalErrors()}. + */ + public float getFatalErrorRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * fatalErrorCount / playTimeMs; + } + + /** + * Returns the mean play time between fatal errors, in seconds. This is equivalent to 1.0 / {@link + * #getFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenFatalErrors() { + return 1f / getFatalErrorRate(); + } + + /** + * Returns the mean number of non-fatal errors per foreground playback, or {@code 0.0} if no + * playback has been in foreground. + */ + public float getMeanNonFatalErrorCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) nonFatalErrorCount / foregroundPlaybackCount; + } + + /** + * Returns the rate of non-fatal errors, in errors per play time second, or {@code 0.0} if no time + * was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenNonFatalErrors()}. + */ + public float getNonFatalErrorRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * nonFatalErrorCount / playTimeMs; + } + + /** + * Returns the mean play time between non-fatal errors, in seconds. This is equivalent to 1.0 / + * {@link #getNonFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenNonFatalErrors() { + return 1f / getNonFatalErrorRate(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java new file mode 100644 index 0000000000..6444b4747f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -0,0 +1,1005 @@ +/* + * 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.analytics; + +import android.os.SystemClock; +import androidx.annotation.Nullable; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** + * {@link AnalyticsListener} to gather {@link PlaybackStats} from the player. + * + *

For accurate measurements, the listener should be added to the player before loading media, + * i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}. + * + *

Playback stats are gathered separately for all playback session, i.e. each window in the + * {@link Timeline} and each single ad. + */ +public final class PlaybackStatsListener + implements AnalyticsListener, PlaybackSessionManager.Listener { + + /** A listener for {@link PlaybackStats} updates. */ + public interface Callback { + + /** + * Called when a playback session ends and its {@link PlaybackStats} are ready. + * + * @param eventTime The {@link EventTime} at which the playback session started. Can be used to + * identify the playback session. + * @param playbackStats The {@link PlaybackStats} for the ended playback session. + */ + void onPlaybackStatsReady(EventTime eventTime, PlaybackStats playbackStats); + } + + private final PlaybackSessionManager sessionManager; + private final Map playbackStatsTrackers; + private final Map sessionStartEventTimes; + @Nullable private final Callback callback; + private final boolean keepHistory; + private final Period period; + + private PlaybackStats finishedPlaybackStats; + @Nullable private String activeContentPlayback; + @Nullable private String activeAdPlayback; + private boolean playWhenReady; + @Player.State private int playbackState; + private float playbackSpeed; + + /** + * Creates listener for playback stats. + * + * @param keepHistory Whether the reported {@link PlaybackStats} should keep the full history of + * events. + * @param callback An optional callback for finished {@link PlaybackStats}. + */ + public PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback) { + this.callback = callback; + this.keepHistory = keepHistory; + sessionManager = new DefaultPlaybackSessionManager(); + playbackStatsTrackers = new HashMap<>(); + sessionStartEventTimes = new HashMap<>(); + finishedPlaybackStats = PlaybackStats.EMPTY; + playWhenReady = false; + playbackState = Player.STATE_IDLE; + playbackSpeed = 1f; + period = new Period(); + sessionManager.setListener(this); + } + + /** + * Returns the combined {@link PlaybackStats} for all playback sessions this listener was and is + * listening to. + * + *

Note that these {@link PlaybackStats} will not contain the full history of events. + * + * @return The combined {@link PlaybackStats} for all playback sessions. + */ + public PlaybackStats getCombinedPlaybackStats() { + PlaybackStats[] allPendingPlaybackStats = new PlaybackStats[playbackStatsTrackers.size() + 1]; + allPendingPlaybackStats[0] = finishedPlaybackStats; + int index = 1; + for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { + allPendingPlaybackStats[index++] = tracker.build(/* isFinal= */ false); + } + return PlaybackStats.merge(allPendingPlaybackStats); + } + + /** + * Returns the {@link PlaybackStats} for the currently playback session, or null if no session is + * active. + * + * @return {@link PlaybackStats} for the current playback session. + */ + @Nullable + public PlaybackStats getPlaybackStats() { + PlaybackStatsTracker activeStatsTracker = + activeAdPlayback != null + ? playbackStatsTrackers.get(activeAdPlayback) + : activeContentPlayback != null + ? playbackStatsTrackers.get(activeContentPlayback) + : null; + return activeStatsTracker == null ? null : activeStatsTracker.build(/* isFinal= */ false); + } + + /** + * Finishes all pending playback sessions. Should be called when the listener is removed from the + * player or when the player is released. + */ + public void finishAllSessions() { + // TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with + // an actual EventTime. Should also simplify other cases where the listener needs to be released + // separately from the player. + HashMap trackerCopy = new HashMap<>(playbackStatsTrackers); + EventTime dummyEventTime = + new EventTime( + SystemClock.elapsedRealtime(), + Timeline.EMPTY, + /* windowIndex= */ 0, + /* mediaPeriodId= */ null, + /* eventPlaybackPositionMs= */ 0, + /* currentPlaybackPositionMs= */ 0, + /* totalBufferedDurationMs= */ 0); + for (String session : trackerCopy.keySet()) { + onSessionFinished(dummyEventTime, session, /* automaticTransition= */ false); + } + } + + // PlaybackSessionManager.Listener implementation. + + @Override + public void onSessionCreated(EventTime eventTime, String session) { + PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); + tracker.onPlayerStateChanged( + eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true); + tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); + playbackStatsTrackers.put(session, tracker); + sessionStartEventTimes.put(session, eventTime); + } + + @Override + public void onSessionActive(EventTime eventTime, String session) { + Assertions.checkNotNull(playbackStatsTrackers.get(session)).onForeground(eventTime); + if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) { + activeAdPlayback = session; + } else { + activeContentPlayback = session; + } + } + + @Override + public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) { + Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd()); + long contentPositionUs = + eventTime + .timeline + .getPeriodByUid(eventTime.mediaPeriodId.periodUid, period) + .getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex); + EventTime contentEventTime = + new EventTime( + eventTime.realtimeMs, + eventTime.timeline, + eventTime.windowIndex, + new MediaPeriodId( + eventTime.mediaPeriodId.periodUid, + eventTime.mediaPeriodId.windowSequenceNumber, + eventTime.mediaPeriodId.adGroupIndex), + /* eventPlaybackPositionMs= */ C.usToMs(contentPositionUs), + eventTime.currentPlaybackPositionMs, + eventTime.totalBufferedDurationMs); + Assertions.checkNotNull(playbackStatsTrackers.get(contentSession)) + .onSuspended(contentEventTime, /* belongsToPlayback= */ true); + } + + @Override + public void onSessionFinished(EventTime eventTime, String session, boolean automaticTransition) { + if (session.equals(activeAdPlayback)) { + activeAdPlayback = null; + } else if (session.equals(activeContentPlayback)) { + activeContentPlayback = null; + } + PlaybackStatsTracker tracker = Assertions.checkNotNull(playbackStatsTrackers.remove(session)); + EventTime startEventTime = Assertions.checkNotNull(sessionStartEventTimes.remove(session)); + if (automaticTransition) { + // Simulate ENDED state to record natural ending of playback. + tracker.onPlayerStateChanged( + eventTime, /* playWhenReady= */ true, Player.STATE_ENDED, /* belongsToPlayback= */ false); + } + tracker.onSuspended(eventTime, /* belongsToPlayback= */ false); + PlaybackStats playbackStats = tracker.build(/* isFinal= */ true); + finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats); + if (callback != null) { + callback.onPlaybackStatsReady(startEventTime, playbackStats); + } + } + + // AnalyticsListener implementation. + + @Override + public void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) { + this.playWhenReady = playWhenReady; + this.playbackState = playbackState; + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers + .get(session) + .onPlayerStateChanged(eventTime, playWhenReady, playbackState, belongsToPlayback); + } + } + + @Override + public void onTimelineChanged(EventTime eventTime, int reason) { + sessionManager.handleTimelineUpdate(eventTime); + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); + } + } + } + + @Override + public void onPositionDiscontinuity(EventTime eventTime, int reason) { + sessionManager.handlePositionDiscontinuity(eventTime, reason); + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); + } + } + } + + @Override + public void onSeekStarted(EventTime eventTime) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onSeekStarted(eventTime); + } + } + } + + @Override + public void onSeekProcessed(EventTime eventTime) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onSeekProcessed(eventTime); + } + } + } + + @Override + public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onFatalError(eventTime, error); + } + } + } + + @Override + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + playbackSpeed = playbackParameters.speed; + sessionManager.updateSessions(eventTime); + for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { + tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); + } + } + + @Override + public void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections); + } + } + } + + @Override + public void onLoadStarted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onLoadStarted(eventTime); + } + } + } + + @Override + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData); + } + } + } + + @Override + public void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height); + } + } + } + + @Override + public void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded); + } + } + } + + @Override + public void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onAudioUnderrun(); + } + } + } + + @Override + public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames); + } + } + } + + @Override + public void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); + } + } + } + + @Override + public void onDrmSessionManagerError(EventTime eventTime, Exception error) { + sessionManager.updateSessions(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); + } + } + } + + /** Tracker for playback stats of a single playback. */ + private static final class PlaybackStatsTracker { + + // Final stats. + private final boolean keepHistory; + private final long[] playbackStateDurationsMs; + private final List> playbackStateHistory; + private final List mediaTimeHistory; + private final List> videoFormatHistory; + private final List> audioFormatHistory; + private final List> fatalErrorHistory; + private final List> nonFatalErrorHistory; + private final boolean isAd; + + private long firstReportedTimeMs; + private boolean hasBeenReady; + private boolean hasEnded; + private boolean isJoinTimeInvalid; + private int pauseCount; + private int pauseBufferCount; + private int seekCount; + private int rebufferCount; + private long maxRebufferTimeMs; + private int initialVideoFormatHeight; + private long initialVideoFormatBitrate; + private long initialAudioFormatBitrate; + private long videoFormatHeightTimeMs; + private long videoFormatHeightTimeProduct; + private long videoFormatBitrateTimeMs; + private long videoFormatBitrateTimeProduct; + private long audioFormatTimeMs; + private long audioFormatBitrateTimeProduct; + private long bandwidthTimeMs; + private long bandwidthBytes; + private long droppedFrames; + private long audioUnderruns; + private int fatalErrorCount; + private int nonFatalErrorCount; + + // Current player state tracking. + private @PlaybackState int currentPlaybackState; + private long currentPlaybackStateStartTimeMs; + private boolean isSeeking; + private boolean isForeground; + private boolean isSuspended; + private boolean playWhenReady; + @Player.State private int playerPlaybackState; + private boolean hasFatalError; + private boolean startedLoading; + private long lastRebufferStartTimeMs; + @Nullable private Format currentVideoFormat; + @Nullable private Format currentAudioFormat; + private long lastVideoFormatStartTimeMs; + private long lastAudioFormatStartTimeMs; + private float currentPlaybackSpeed; + + /** + * Creates a tracker for playback stats. + * + * @param keepHistory Whether to keep a full history of events. + * @param startTime The {@link EventTime} at which the playback stats start. + */ + public PlaybackStatsTracker(boolean keepHistory, EventTime startTime) { + this.keepHistory = keepHistory; + playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT]; + playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + mediaTimeHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + videoFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + audioFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + fatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + nonFatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + currentPlaybackState = PlaybackStats.PLAYBACK_STATE_NOT_STARTED; + currentPlaybackStateStartTimeMs = startTime.realtimeMs; + playerPlaybackState = Player.STATE_IDLE; + firstReportedTimeMs = C.TIME_UNSET; + maxRebufferTimeMs = C.TIME_UNSET; + isAd = startTime.mediaPeriodId != null && startTime.mediaPeriodId.isAd(); + initialAudioFormatBitrate = C.LENGTH_UNSET; + initialVideoFormatBitrate = C.LENGTH_UNSET; + initialVideoFormatHeight = C.LENGTH_UNSET; + currentPlaybackSpeed = 1f; + } + + /** + * Notifies the tracker of a player state change event, including all player state changes while + * the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param playWhenReady Whether the playback will proceed when ready. + * @param playbackState The current {@link Player.State}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. + */ + public void onPlayerStateChanged( + EventTime eventTime, + boolean playWhenReady, + @Player.State int playbackState, + boolean belongsToPlayback) { + this.playWhenReady = playWhenReady; + playerPlaybackState = playbackState; + if (playbackState != Player.STATE_IDLE) { + hasFatalError = false; + } + if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) { + isSuspended = false; + } + maybeUpdatePlaybackState(eventTime, belongsToPlayback); + } + + /** + * Notifies the tracker of a position discontinuity or timeline update for the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onPositionDiscontinuity(EventTime eventTime) { + isSuspended = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of the start of a seek in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onSeekStarted(EventTime eventTime) { + isSeeking = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of a seek has been processed in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onSeekProcessed(EventTime eventTime) { + isSeeking = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of fatal player error in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onFatalError(EventTime eventTime, Exception error) { + fatalErrorCount++; + if (keepHistory) { + fatalErrorHistory.add(Pair.create(eventTime, error)); + } + hasFatalError = true; + isSuspended = false; + isSeeking = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that a load for the current playback has started. + * + * @param eventTime The {@link EventTime}. + */ + public void onLoadStarted(EventTime eventTime) { + startedLoading = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that the current playback became the active foreground playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onForeground(EventTime eventTime) { + isForeground = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that the current playback has been suspended, e.g. for ad playback or + * permanently. + * + * @param eventTime The {@link EventTime}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. + */ + public void onSuspended(EventTime eventTime, boolean belongsToPlayback) { + isSuspended = true; + isSeeking = false; + maybeUpdatePlaybackState(eventTime, belongsToPlayback); + } + + /** + * Notifies the tracker that the track selection for the current playback changed. + * + * @param eventTime The {@link EventTime}. + * @param trackSelections The new {@link TrackSelectionArray}. + */ + public void onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections) { + boolean videoEnabled = false; + boolean audioEnabled = false; + for (TrackSelection trackSelection : trackSelections.getAll()) { + if (trackSelection != null && trackSelection.length() > 0) { + int trackType = MimeTypes.getTrackType(trackSelection.getFormat(0).sampleMimeType); + if (trackType == C.TRACK_TYPE_VIDEO) { + videoEnabled = true; + } else if (trackType == C.TRACK_TYPE_AUDIO) { + audioEnabled = true; + } + } + } + if (!videoEnabled) { + maybeUpdateVideoFormat(eventTime, /* newFormat= */ null); + } + if (!audioEnabled) { + maybeUpdateAudioFormat(eventTime, /* newFormat= */ null); + } + } + + /** + * Notifies the tracker that a format being read by the renderers for the current playback + * changed. + * + * @param eventTime The {@link EventTime}. + * @param mediaLoadData The {@link MediaLoadData} describing the format change. + */ + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO + || mediaLoadData.trackType == C.TRACK_TYPE_DEFAULT) { + maybeUpdateVideoFormat(eventTime, mediaLoadData.trackFormat); + } else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) { + maybeUpdateAudioFormat(eventTime, mediaLoadData.trackFormat); + } + } + + /** + * Notifies the tracker that the video size for the current playback changed. + * + * @param eventTime The {@link EventTime}. + * @param width The video width in pixels. + * @param height The video height in pixels. + */ + public void onVideoSizeChanged(EventTime eventTime, int width, int height) { + if (currentVideoFormat != null && currentVideoFormat.height == Format.NO_VALUE) { + Format formatWithHeight = currentVideoFormat.copyWithVideoSize(width, height); + maybeUpdateVideoFormat(eventTime, formatWithHeight); + } + } + + /** + * Notifies the tracker of a playback speed change, including all playback speed changes while + * the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param playbackSpeed The new playback speed. + */ + public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { + maybeUpdateMediaTimeHistory(eventTime.realtimeMs, eventTime.eventPlaybackPositionMs); + maybeRecordVideoFormatTime(eventTime.realtimeMs); + maybeRecordAudioFormatTime(eventTime.realtimeMs); + currentPlaybackSpeed = playbackSpeed; + } + + /** Notifies the builder of an audio underrun for the current playback. */ + public void onAudioUnderrun() { + audioUnderruns++; + } + + /** + * Notifies the tracker of dropped video frames for the current playback. + * + * @param droppedFrames The number of dropped video frames. + */ + public void onDroppedVideoFrames(int droppedFrames) { + this.droppedFrames += droppedFrames; + } + + /** + * Notifies the tracker of bandwidth measurement data for the current playback. + * + * @param timeMs The time for which bandwidth measurement data is available, in milliseconds. + * @param bytes The bytes transferred during {@code timeMs}. + */ + public void onBandwidthData(long timeMs, long bytes) { + bandwidthTimeMs += timeMs; + bandwidthBytes += bytes; + } + + /** + * Notifies the tracker of a non-fatal error in the current playback. + * + * @param eventTime The {@link EventTime}. + * @param error The error. + */ + public void onNonFatalError(EventTime eventTime, Exception error) { + nonFatalErrorCount++; + if (keepHistory) { + nonFatalErrorHistory.add(Pair.create(eventTime, error)); + } + } + + /** + * Builds the playback stats. + * + * @param isFinal Whether this is the final build and no further events are expected. + */ + public PlaybackStats build(boolean isFinal) { + long[] playbackStateDurationsMs = this.playbackStateDurationsMs; + List mediaTimeHistory = this.mediaTimeHistory; + if (!isFinal) { + long buildTimeMs = SystemClock.elapsedRealtime(); + playbackStateDurationsMs = + Arrays.copyOf(this.playbackStateDurationsMs, PlaybackStats.PLAYBACK_STATE_COUNT); + long lastStateDurationMs = Math.max(0, buildTimeMs - currentPlaybackStateStartTimeMs); + playbackStateDurationsMs[currentPlaybackState] += lastStateDurationMs; + maybeUpdateMaxRebufferTimeMs(buildTimeMs); + maybeRecordVideoFormatTime(buildTimeMs); + maybeRecordAudioFormatTime(buildTimeMs); + mediaTimeHistory = new ArrayList<>(this.mediaTimeHistory); + if (keepHistory && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING) { + mediaTimeHistory.add(guessMediaTimeBasedOnElapsedRealtime(buildTimeMs)); + } + } + boolean isJoinTimeInvalid = this.isJoinTimeInvalid || !hasBeenReady; + long validJoinTimeMs = + isJoinTimeInvalid + ? C.TIME_UNSET + : playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND]; + boolean hasBackgroundJoin = + playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0; + List> videoHistory = + isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory); + List> audioHistory = + isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory); + return new PlaybackStats( + /* playbackCount= */ 1, + playbackStateDurationsMs, + isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory), + mediaTimeHistory, + firstReportedTimeMs, + /* foregroundPlaybackCount= */ isForeground ? 1 : 0, + /* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1, + /* endedCount= */ hasEnded ? 1 : 0, + /* backgroundJoiningCount= */ hasBackgroundJoin ? 1 : 0, + validJoinTimeMs, + /* validJoinTimeCount= */ isJoinTimeInvalid ? 0 : 1, + pauseCount, + pauseBufferCount, + seekCount, + rebufferCount, + maxRebufferTimeMs, + /* adPlaybackCount= */ isAd ? 1 : 0, + videoHistory, + audioHistory, + videoFormatHeightTimeMs, + videoFormatHeightTimeProduct, + videoFormatBitrateTimeMs, + videoFormatBitrateTimeProduct, + audioFormatTimeMs, + audioFormatBitrateTimeProduct, + /* initialVideoFormatHeightCount= */ initialVideoFormatHeight == C.LENGTH_UNSET ? 0 : 1, + /* initialVideoFormatBitrateCount= */ initialVideoFormatBitrate == C.LENGTH_UNSET ? 0 : 1, + initialVideoFormatHeight, + initialVideoFormatBitrate, + /* initialAudioFormatBitrateCount= */ initialAudioFormatBitrate == C.LENGTH_UNSET ? 0 : 1, + initialAudioFormatBitrate, + bandwidthTimeMs, + bandwidthBytes, + droppedFrames, + audioUnderruns, + /* fatalErrorPlaybackCount= */ fatalErrorCount > 0 ? 1 : 0, + fatalErrorCount, + nonFatalErrorCount, + fatalErrorHistory, + nonFatalErrorHistory); + } + + private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) { + @PlaybackState int newPlaybackState = resolveNewPlaybackState(); + if (newPlaybackState == currentPlaybackState) { + return; + } + Assertions.checkArgument(eventTime.realtimeMs >= currentPlaybackStateStartTimeMs); + + long stateDurationMs = eventTime.realtimeMs - currentPlaybackStateStartTimeMs; + playbackStateDurationsMs[currentPlaybackState] += stateDurationMs; + if (firstReportedTimeMs == C.TIME_UNSET) { + firstReportedTimeMs = eventTime.realtimeMs; + } + isJoinTimeInvalid |= isInvalidJoinTransition(currentPlaybackState, newPlaybackState); + hasBeenReady |= isReadyState(newPlaybackState); + hasEnded |= newPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED; + if (!isPausedState(currentPlaybackState) && isPausedState(newPlaybackState)) { + pauseCount++; + } + if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING) { + seekCount++; + } + if (!isRebufferingState(currentPlaybackState) && isRebufferingState(newPlaybackState)) { + rebufferCount++; + lastRebufferStartTimeMs = eventTime.realtimeMs; + } + if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING + && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_BUFFERING) { + pauseBufferCount++; + } + + maybeUpdateMediaTimeHistory( + eventTime.realtimeMs, + /* mediaTimeMs= */ belongsToPlayback ? eventTime.eventPlaybackPositionMs : C.TIME_UNSET); + maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs); + maybeRecordVideoFormatTime(eventTime.realtimeMs); + maybeRecordAudioFormatTime(eventTime.realtimeMs); + + currentPlaybackState = newPlaybackState; + currentPlaybackStateStartTimeMs = eventTime.realtimeMs; + if (keepHistory) { + playbackStateHistory.add(Pair.create(eventTime, currentPlaybackState)); + } + } + + private @PlaybackState int resolveNewPlaybackState() { + if (isSuspended) { + // Keep VIDEO_STATE_ENDED if playback naturally ended (or progressed to next item). + return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED + ? PlaybackStats.PLAYBACK_STATE_ENDED + : PlaybackStats.PLAYBACK_STATE_SUSPENDED; + } else if (isSeeking) { + // Seeking takes precedence over errors such that we report a seek while in error state. + return PlaybackStats.PLAYBACK_STATE_SEEKING; + } else if (hasFatalError) { + return PlaybackStats.PLAYBACK_STATE_FAILED; + } else if (!isForeground) { + // Before the playback becomes foreground, only report background joining and not started. + return startedLoading + ? PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + : PlaybackStats.PLAYBACK_STATE_NOT_STARTED; + } else if (playerPlaybackState == Player.STATE_ENDED) { + return PlaybackStats.PLAYBACK_STATE_ENDED; + } else if (playerPlaybackState == Player.STATE_BUFFERING) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_NOT_STARTED + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SUSPENDED) { + return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND; + } + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING) { + return PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING; + } + return playWhenReady + ? PlaybackStats.PLAYBACK_STATE_BUFFERING + : PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } else if (playerPlaybackState == Player.STATE_READY) { + return playWhenReady + ? PlaybackStats.PLAYBACK_STATE_PLAYING + : PlaybackStats.PLAYBACK_STATE_PAUSED; + } else if (playerPlaybackState == Player.STATE_IDLE + && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_NOT_STARTED) { + // This case only applies for calls to player.stop(). All other IDLE cases are handled by + // !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored. + return PlaybackStats.PLAYBACK_STATE_STOPPED; + } + return currentPlaybackState; + } + + private void maybeUpdateMaxRebufferTimeMs(long nowMs) { + if (isRebufferingState(currentPlaybackState)) { + long rebufferDurationMs = nowMs - lastRebufferStartTimeMs; + if (maxRebufferTimeMs == C.TIME_UNSET || rebufferDurationMs > maxRebufferTimeMs) { + maxRebufferTimeMs = rebufferDurationMs; + } + } + } + + private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) { + if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) { + if (mediaTimeMs == C.TIME_UNSET) { + return; + } + if (!mediaTimeHistory.isEmpty()) { + long previousMediaTimeMs = mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1]; + if (previousMediaTimeMs != mediaTimeMs) { + mediaTimeHistory.add(new long[] {realtimeMs, previousMediaTimeMs}); + } + } + } + mediaTimeHistory.add( + mediaTimeMs == C.TIME_UNSET + ? guessMediaTimeBasedOnElapsedRealtime(realtimeMs) + : new long[] {realtimeMs, mediaTimeMs}); + } + + private long[] guessMediaTimeBasedOnElapsedRealtime(long realtimeMs) { + long[] previousKnownMediaTimeHistory = mediaTimeHistory.get(mediaTimeHistory.size() - 1); + long previousRealtimeMs = previousKnownMediaTimeHistory[0]; + long previousMediaTimeMs = previousKnownMediaTimeHistory[1]; + long elapsedMediaTimeEstimateMs = + (long) ((realtimeMs - previousRealtimeMs) * currentPlaybackSpeed); + long mediaTimeEstimateMs = previousMediaTimeMs + elapsedMediaTimeEstimateMs; + return new long[] {realtimeMs, mediaTimeEstimateMs}; + } + + private void maybeUpdateVideoFormat(EventTime eventTime, @Nullable Format newFormat) { + if (Util.areEqual(currentVideoFormat, newFormat)) { + return; + } + maybeRecordVideoFormatTime(eventTime.realtimeMs); + if (newFormat != null) { + if (initialVideoFormatHeight == C.LENGTH_UNSET && newFormat.height != Format.NO_VALUE) { + initialVideoFormatHeight = newFormat.height; + } + if (initialVideoFormatBitrate == C.LENGTH_UNSET && newFormat.bitrate != Format.NO_VALUE) { + initialVideoFormatBitrate = newFormat.bitrate; + } + } + currentVideoFormat = newFormat; + if (keepHistory) { + videoFormatHistory.add(Pair.create(eventTime, currentVideoFormat)); + } + } + + private void maybeUpdateAudioFormat(EventTime eventTime, @Nullable Format newFormat) { + if (Util.areEqual(currentAudioFormat, newFormat)) { + return; + } + maybeRecordAudioFormatTime(eventTime.realtimeMs); + if (newFormat != null + && initialAudioFormatBitrate == C.LENGTH_UNSET + && newFormat.bitrate != Format.NO_VALUE) { + initialAudioFormatBitrate = newFormat.bitrate; + } + currentAudioFormat = newFormat; + if (keepHistory) { + audioFormatHistory.add(Pair.create(eventTime, currentAudioFormat)); + } + } + + private void maybeRecordVideoFormatTime(long nowMs) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING + && currentVideoFormat != null) { + long mediaDurationMs = (long) ((nowMs - lastVideoFormatStartTimeMs) * currentPlaybackSpeed); + if (currentVideoFormat.height != Format.NO_VALUE) { + videoFormatHeightTimeMs += mediaDurationMs; + videoFormatHeightTimeProduct += mediaDurationMs * currentVideoFormat.height; + } + if (currentVideoFormat.bitrate != Format.NO_VALUE) { + videoFormatBitrateTimeMs += mediaDurationMs; + videoFormatBitrateTimeProduct += mediaDurationMs * currentVideoFormat.bitrate; + } + } + lastVideoFormatStartTimeMs = nowMs; + } + + private void maybeRecordAudioFormatTime(long nowMs) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING + && currentAudioFormat != null + && currentAudioFormat.bitrate != Format.NO_VALUE) { + long mediaDurationMs = (long) ((nowMs - lastAudioFormatStartTimeMs) * currentPlaybackSpeed); + audioFormatTimeMs += mediaDurationMs; + audioFormatBitrateTimeProduct += mediaDurationMs * currentAudioFormat.bitrate; + } + lastAudioFormatStartTimeMs = nowMs; + } + + private static boolean isReadyState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_PLAYING + || state == PlaybackStats.PLAYBACK_STATE_PAUSED; + } + + private static boolean isPausedState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_PAUSED + || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } + + private static boolean isRebufferingState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_BUFFERING + || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } + + private static boolean isInvalidJoinTransition( + @PlaybackState int oldState, @PlaybackState int newState) { + if (oldState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + && oldState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + && oldState != PlaybackStats.PLAYBACK_STATE_SUSPENDED) { + return false; + } + return newState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + && newState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + && newState != PlaybackStats.PLAYBACK_STATE_SUSPENDED + && newState != PlaybackStats.PLAYBACK_STATE_PLAYING + && newState != PlaybackStats.PLAYBACK_STATE_PAUSED + && newState != PlaybackStats.PLAYBACK_STATE_ENDED; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java index 3cc05e87df..44bcdfd495 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java @@ -103,11 +103,11 @@ public final class AudioFocusManager { private final AudioManager audioManager; private final AudioFocusListener focusListener; private final PlayerControl playerControl; - private @Nullable AudioAttributes audioAttributes; + @Nullable private AudioAttributes audioAttributes; - private @AudioFocusState int audioFocusState; - private int focusGain; - private float volumeMultiplier = 1.0f; + @AudioFocusState private int audioFocusState; + @C.AudioFocusGain private int focusGain; + private float volumeMultiplier = VOLUME_MULTIPLIER_DEFAULT; private @MonotonicNonNull AudioFocusRequest audioFocusRequest; private boolean rebuildAudioFocusRequest; @@ -310,6 +310,7 @@ public final class AudioFocusManager { * @param audioAttributes The audio attributes associated with this focus request. * @return The type of audio focus gain that should be requested. */ + @C.AudioFocusGain private static int convertAudioAttributesToFocusGain(@Nullable AudioAttributes audioAttributes) { if (audioAttributes == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 393380453c..f2458a7471 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -259,13 +259,12 @@ public interface AudioSink { boolean hasPendingData(); /** - * Attempts to set the playback parameters and returns the active playback parameters, which may - * differ from those passed in. + * Attempts to set the playback parameters. The audio sink may override these parameters if they + * are not supported. * * @param playbackParameters The new playback parameters to attempt to set. - * @return The active playback parameters. */ - PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters); + void setPlaybackParameters(PlaybackParameters playbackParameters); /** * Gets the active {@link PlaybackParameters}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java index d43972d7b0..0564591f1f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java @@ -82,7 +82,7 @@ import java.lang.annotation.RetentionPolicy; */ private static final int INITIALIZING_DURATION_US = 500_000; - private final @Nullable AudioTimestampV19 audioTimestamp; + @Nullable private final AudioTimestampV19 audioTimestamp; private @State int state; private long initializeSystemTimeUs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index e87e49d2da..4ee70bd813 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -133,10 +133,10 @@ import java.lang.reflect.Method; private final Listener listener; private final long[] playheadOffsets; - private @Nullable AudioTrack audioTrack; + @Nullable private AudioTrack audioTrack; private int outputPcmFrameSize; private int bufferSize; - private @Nullable AudioTimestampPoller audioTimestampPoller; + @Nullable private AudioTimestampPoller audioTimestampPoller; private int outputSampleRate; private boolean needsPassthroughWorkarounds; private long bufferSizeUs; @@ -144,7 +144,7 @@ import java.lang.reflect.Method; private long smoothedPlayheadOffsetUs; private long lastPlayheadSampleTimeUs; - private @Nullable Method getLatencyMethod; + @Nullable private Method getLatencyMethod; private long latencyUs; private boolean hasData; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index cf914567d6..b4e0058982 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -501,7 +501,7 @@ public final class DefaultAudioSink implements AudioSink { } } - private void initialize() throws InitializationException { + private void initialize(long presentationTimeUs) throws InitializationException { // If we're asynchronously releasing a previous audio track then we block until it has been // released. This guarantees that we cannot end up in a state where we have multiple audio // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust @@ -533,11 +533,7 @@ public final class DefaultAudioSink implements AudioSink { } } - playbackParameters = - configuration.canApplyPlaybackParameters - ? audioProcessorChain.applyPlaybackParameters(playbackParameters) - : PlaybackParameters.DEFAULT; - setupAudioProcessors(); + applyPlaybackParameters(playbackParameters, presentationTimeUs); audioTrackPositionTracker.setAudioTrack( audioTrack, @@ -591,15 +587,12 @@ public final class DefaultAudioSink implements AudioSink { configuration = pendingConfiguration; pendingConfiguration = null; } - playbackParameters = - configuration.canApplyPlaybackParameters - ? audioProcessorChain.applyPlaybackParameters(playbackParameters) - : PlaybackParameters.DEFAULT; - setupAudioProcessors(); + // Re-apply playback parameters. + applyPlaybackParameters(playbackParameters, presentationTimeUs); } if (!isInitialized()) { - initialize(); + initialize(presentationTimeUs); if (playing) { play(); } @@ -635,15 +628,7 @@ public final class DefaultAudioSink implements AudioSink { } PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters; afterDrainPlaybackParameters = null; - newPlaybackParameters = audioProcessorChain.applyPlaybackParameters(newPlaybackParameters); - // Store the position and corresponding media time from which the parameters will apply. - playbackParametersCheckpoints.add( - new PlaybackParametersCheckpoint( - newPlaybackParameters, - Math.max(0, presentationTimeUs), - configuration.framesToDurationUs(getWrittenFrames()))); - // Update the set of active audio processors to take into account the new parameters. - setupAudioProcessors(); + applyPlaybackParameters(newPlaybackParameters, presentationTimeUs); } if (startMediaTimeState == START_NOT_SET) { @@ -840,33 +825,33 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (configuration != null && !configuration.canApplyPlaybackParameters) { this.playbackParameters = PlaybackParameters.DEFAULT; - return this.playbackParameters; + return; } - PlaybackParameters lastSetPlaybackParameters = - afterDrainPlaybackParameters != null - ? afterDrainPlaybackParameters - : !playbackParametersCheckpoints.isEmpty() - ? playbackParametersCheckpoints.getLast().playbackParameters - : this.playbackParameters; + PlaybackParameters lastSetPlaybackParameters = getPlaybackParameters(); if (!playbackParameters.equals(lastSetPlaybackParameters)) { if (isInitialized()) { // Drain the audio processors so we can determine the frame position at which the new // parameters apply. afterDrainPlaybackParameters = playbackParameters; } else { - // Update the playback parameters now. - this.playbackParameters = audioProcessorChain.applyPlaybackParameters(playbackParameters); + // Update the playback parameters now. They will be applied to the audio processors during + // initialization. + this.playbackParameters = playbackParameters; } } - return this.playbackParameters; } @Override public PlaybackParameters getPlaybackParameters() { - return playbackParameters; + // Mask the already set parameters. + return afterDrainPlaybackParameters != null + ? afterDrainPlaybackParameters + : !playbackParametersCheckpoints.isEmpty() + ? playbackParametersCheckpoints.getLast().playbackParameters + : playbackParameters; } @Override @@ -1040,6 +1025,21 @@ public final class DefaultAudioSink implements AudioSink { }.start(); } + private void applyPlaybackParameters( + PlaybackParameters playbackParameters, long presentationTimeUs) { + PlaybackParameters newPlaybackParameters = + configuration.canApplyPlaybackParameters + ? audioProcessorChain.applyPlaybackParameters(playbackParameters) + : PlaybackParameters.DEFAULT; + // Store the position and corresponding media time from which the parameters will apply. + playbackParametersCheckpoints.add( + new PlaybackParametersCheckpoint( + newPlaybackParameters, + /* mediaTimeUs= */ Math.max(0, presentationTimeUs), + /* positionUs= */ configuration.framesToDurationUs(getWrittenFrames()))); + setupAudioProcessors(); + } + private long applySpeedup(long positionUs) { @Nullable PlaybackParametersCheckpoint checkpoint = null; while (!playbackParametersCheckpoints.isEmpty() @@ -1125,6 +1125,7 @@ public final class DefaultAudioSink implements AudioSink { case C.ENCODING_AC3: return 640 * 1000 / 8; case C.ENCODING_E_AC3: + case C.ENCODING_E_AC3_JOC: return 6144 * 1000 / 8; case C.ENCODING_AC4: return 2688 * 1000 / 8; @@ -1154,7 +1155,7 @@ public final class DefaultAudioSink implements AudioSink { return DtsUtil.parseDtsAudioSampleCount(buffer); } else if (encoding == C.ENCODING_AC3) { return Ac3Util.getAc3SyncframeAudioSampleCount(); - } else if (encoding == C.ENCODING_E_AC3) { + } else if (encoding == C.ENCODING_E_AC3 || encoding == C.ENCODING_E_AC3_JOC) { return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); } else if (encoding == C.ENCODING_AC4) { return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); @@ -1177,11 +1178,10 @@ public final class DefaultAudioSink implements AudioSink { @TargetApi(21) private int writeNonBlockingWithAvSyncV21(AudioTrack audioTrack, ByteBuffer buffer, int size, long presentationTimeUs) { - // TODO: Uncomment this when [Internal ref: b/33627517] is clarified or fixed. - // if (Util.SDK_INT >= 23) { - // // The underlying platform AudioTrack writes AV sync headers directly. - // return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000); - // } + if (Util.SDK_INT >= 26) { + // The underlying platform AudioTrack writes AV sync headers directly. + return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000); + } if (avSyncHeader == null) { avSyncHeader = ByteBuffer.allocate(16); avSyncHeader.order(ByteOrder.BIG_ENDIAN); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index c3ec759c2d..6a29f316e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -246,12 +246,50 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + /* enableDecoderFallback= */ false, + eventHandler, + eventListener, + audioSink); + } + + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has 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 to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + */ + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { super( C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - /* enableDecoderFallback= */ false, + enableDecoderFallback, /* assumedMinimumCodecOperatingRate= */ 44100); this.context = context.getApplicationContext(); this.audioSink = audioSink; @@ -270,7 +308,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return FORMAT_UNSUPPORTED_TYPE; } int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - boolean supportsFormatDrm = supportsFormatDrm(drmSessionManager, format.drmInitData); + boolean supportsFormatDrm = + format.drmInitData == null + || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, format.drmInitData)); if (supportsFormatDrm && allowPassthrough(format.channelCount, mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { @@ -336,7 +378,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @return Whether passthrough playback is supported. */ protected boolean allowPassthrough(int channelCount, String mimeType) { - return audioSink.supportsOutput(channelCount, MimeTypes.getEncoding(mimeType)); + return getPassthroughEncoding(channelCount, mimeType) != C.ENCODING_INVALID; } @Override @@ -350,7 +392,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); codecNeedsEosBufferTimestampWorkaround = codecNeedsEosBufferTimestampWorkaround(codecInfo.name); passthroughEnabled = codecInfo.passthrough; - String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.mimeType; + String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.codecMimeType; MediaFormat mediaFormat = getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate); codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); @@ -433,11 +475,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @C.Encoding int encoding; MediaFormat format; if (passthroughMediaFormat != null) { - encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME)); format = passthroughMediaFormat; + encoding = + getPassthroughEncoding( + format.getInteger(MediaFormat.KEY_CHANNEL_COUNT), + format.getString(MediaFormat.KEY_MIME)); } else { - encoding = pcmEncoding; format = outputFormat; + encoding = pcmEncoding; } int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); @@ -459,6 +504,28 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + /** + * Returns the {@link C.Encoding} constant to use for passthrough of the given format, or {@link + * C#ENCODING_INVALID} if passthrough is not possible. + */ + @C.Encoding + protected int getPassthroughEncoding(int channelCount, String mimeType) { + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { + if (audioSink.supportsOutput(channelCount, C.ENCODING_E_AC3_JOC)) { + return MimeTypes.getEncoding(MimeTypes.AUDIO_E_AC3_JOC); + } + // E-AC3 receivers can decode JOC streams, but in 2-D rather than 3-D, so try to fall back. + mimeType = MimeTypes.AUDIO_E_AC3; + } + + @C.Encoding int encoding = MimeTypes.getEncoding(mimeType); + if (audioSink.supportsOutput(channelCount, encoding)) { + return encoding; + } else { + return C.ENCODING_INVALID; + } + } + /** * Called when the audio session id becomes known. The default implementation is a no-op. One * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in @@ -581,8 +648,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return audioSink.setPlaybackParameters(playbackParameters); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + audioSink.setPlaybackParameters(playbackParameters); } @Override @@ -628,7 +695,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException { if (codecNeedsEosBufferTimestampWorkaround @@ -644,7 +712,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return true; } - if (shouldSkip) { + if (isDecodeOnlyBuffer) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.skippedOutputBufferCount++; audioSink.handleDiscontinuity(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 553dfb1187..e4691db7c0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -246,7 +246,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @return The extent to which the renderer supports the format itself. */ protected abstract int supportsFormatInternal( - DrmSessionManager drmSessionManager, Format format); + @Nullable DrmSessionManager drmSessionManager, Format format); /** * Returns whether the sink supports the audio format. @@ -274,7 +274,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements 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()); @@ -341,9 +341,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @return The decoder. * @throws AudioDecoderException If an error occurred creating a suitable decoder. */ - protected abstract SimpleDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) - throws AudioDecoderException; + protected abstract SimpleDecoder< + DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends AudioDecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws AudioDecoderException; /** * Returns the format of audio buffers output by the decoder. Will not be called until the first @@ -438,7 +439,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return false; } if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } if (inputBuffer.isEndOfStream()) { @@ -517,8 +518,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return audioSink.setPlaybackParameters(playbackParameters); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + audioSink.setPlaybackParameters(playbackParameters); } @Override @@ -646,43 +647,39 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } 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); - } - } - - private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { + @SuppressWarnings("unchecked") + private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { Format oldFormat = inputFormat; - inputFormat = newFormat; + inputFormat = formatHolder.format; boolean drmInitDataChanged = !Util.areEqual(inputFormat.drmInitData, oldFormat == null ? null : oldFormat.drmInitData); if (drmInitDataChanged) { if (inputFormat.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(), inputFormat.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); } @@ -698,10 +695,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements audioTrackNeedsConfigure = true; } - encoderDelay = newFormat.encoderDelay; - encoderPadding = newFormat.encoderPadding; + encoderDelay = inputFormat.encoderDelay; + encoderPadding = inputFormat.encoderPadding; - eventDispatcher.inputFormatChanged(newFormat); + eventDispatcher.inputFormatChanged(inputFormat); } private void onQueueInputBuffer(DecoderInputBuffer buffer) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java index 0bf6baa4d0..6cd46bb705 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java @@ -30,6 +30,7 @@ import java.util.Arrays; private static final int MINIMUM_PITCH = 65; private static final int MAXIMUM_PITCH = 400; private static final int AMDF_FREQUENCY = 4000; + private static final int BYTES_PER_SAMPLE = 2; private final int inputSampleRateHz; private final int channelCount; @@ -157,9 +158,9 @@ import java.util.Arrays; maxDiff = 0; } - /** Returns the number of output frames that can be read with {@link #getOutput(ShortBuffer)}. */ - public int getFramesAvailable() { - return outputFrameCount; + /** Returns the size of output that can be read with {@link #getOutput(ShortBuffer)}, in bytes. */ + public int getOutputSize() { + return outputFrameCount * channelCount * BYTES_PER_SAMPLE; } // Internal methods. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java index 0d938d33f4..bd32e5ee6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java @@ -210,7 +210,7 @@ public final class SonicAudioProcessor implements AudioProcessor { sonic.queueInput(shortBuffer); inputBuffer.position(inputBuffer.position() + inputSize); } - int outputSize = sonic.getFramesAvailable() * channelCount * 2; + int outputSize = sonic.getOutputSize(); if (outputSize > 0) { if (buffer.capacity() < outputSize) { buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); @@ -243,7 +243,7 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public boolean isEnded() { - return inputEnded && (sonic == null || sonic.getFramesAvailable() == 0); + return inputEnded && (sonic == null || sonic.getOutputSize() == 0); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index 473a91fedf..f5cabf7c30 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -23,13 +23,13 @@ import com.google.android.exoplayer2.util.Util; public final class WavUtil { /** Four character code for "RIFF". */ - public static final int RIFF_FOURCC = Util.getIntegerCodeForString("RIFF"); + public static final int RIFF_FOURCC = 0x52494646; /** Four character code for "WAVE". */ - public static final int WAVE_FOURCC = Util.getIntegerCodeForString("WAVE"); + public static final int WAVE_FOURCC = 0x57415645; /** Four character code for "fmt ". */ - public static final int FMT_FOURCC = Util.getIntegerCodeForString("fmt "); + public static final int FMT_FOURCC = 0x666d7420; /** Four character code for "data". */ - public static final int DATA_FOURCC = Util.getIntegerCodeForString("data"); + public static final int DATA_FOURCC = 0x64617461; /** WAVE type value for integer PCM audio data. */ private static final int TYPE_PCM = 0x0001; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java index f8204f6be3..b5650860e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -301,5 +301,6 @@ public abstract class SimpleDecoder< * @param reset Whether the decoder must be reset before decoding. * @return A decoder exception if an error occurred, or null if decoding was successful. */ - protected abstract @Nullable E decode(I inputBuffer, O outputBuffer, boolean reset); + @Nullable + protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.java deleted file mode 100644 index dbe5c93172..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.drm; - -/** - * A reference-counted resource used in the decryption of media samples. - * - * @param The reference type with which to make {@link Owner#onLastReferenceReleased} calls. - * Subclasses are expected to pass themselves. - */ -public abstract class DecryptionResource> { - - /** - * Implemented by the class in charge of managing a {@link DecryptionResource resource's} - * lifecycle. - */ - public interface Owner> { - - /** - * Called when the last reference to a {@link DecryptionResource} is {@link #releaseReference() - * released}. - */ - void onLastReferenceReleased(T resource); - } - - // TODO: Consider adding a handler on which the owner should be called. - private final DecryptionResource.Owner owner; - private int referenceCount; - - /** - * Creates a new instance with reference count zero. - * - * @param owner The owner of this instance. - */ - public DecryptionResource(Owner owner) { - this.owner = owner; - referenceCount = 0; - } - - /** Increases by one the reference count for this resource. */ - public void acquireReference() { - referenceCount++; - } - - /** - * Decreases by one the reference count for this resource, and notifies the owner if said count - * reached zero as a result of this operation. - * - *

Must only be called as releasing counter-part of {@link #acquireReference()}. - */ - @SuppressWarnings("unchecked") - public void releaseReference() { - if (--referenceCount == 0) { - owner.onLastReferenceReleased((T) this); - } else if (referenceCount < 0) { - throw new IllegalStateException("Illegal release of resource."); - } - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 215a48fc50..c83214c8d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -38,13 +38,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) -/* package */ class DefaultDrmSession implements DrmSession { +public class DefaultDrmSession implements DrmSession { /** Manages provisioning requests. */ public interface ProvisioningManager { @@ -74,7 +75,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; public interface ReleaseCallback { /** - * Called when the session is released. + * Called immediately after releasing session resources. * * @param session The session. */ @@ -85,16 +86,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private static final int MSG_PROVISION = 0; private static final int MSG_KEYS = 1; - private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; + private static final int MAX_LICENSE_DURATION_TO_RENEW_SECONDS = 60; /** The DRM scheme datas, or null if this session uses offline keys. */ - public final @Nullable List schemeDatas; + @Nullable public final List schemeDatas; private final ExoMediaDrm mediaDrm; private final ProvisioningManager provisioningManager; private final ReleaseCallback releaseCallback; private final @DefaultDrmSessionManager.Mode int mode; - private final @Nullable HashMap optionalKeyRequestParameters; + @Nullable private final HashMap optionalKeyRequestParameters; private final EventDispatcher eventDispatcher; private final int initialDrmRequestRetryCount; @@ -103,16 +104,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /* package */ final PostResponseHandler postResponseHandler; private @DrmSession.State int state; - private int openCount; - private HandlerThread requestHandlerThread; - private PostRequestHandler postRequestHandler; - private @Nullable T mediaCrypto; - private @Nullable DrmSessionException lastException; - private byte @MonotonicNonNull [] sessionId; + private int referenceCount; + @Nullable private HandlerThread requestHandlerThread; + @Nullable private PostRequestHandler postRequestHandler; + @Nullable private T mediaCrypto; + @Nullable private DrmSessionException lastException; + private byte @NullableType [] sessionId; private byte @MonotonicNonNull [] offlineLicenseKeySetId; - private @Nullable KeyRequest currentKeyRequest; - private @Nullable ProvisionRequest currentProvisionRequest; + @Nullable private KeyRequest currentKeyRequest; + @Nullable private ProvisionRequest currentProvisionRequest; /** * Instantiates a new DRM session. @@ -166,47 +167,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; this.eventDispatcher = eventDispatcher; state = STATE_OPENING; - postResponseHandler = new PostResponseHandler(playbackLooper); - requestHandlerThread = new HandlerThread("DrmRequestHandler"); - requestHandlerThread.start(); - postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - } - - // Life cycle. - - public void acquire() { - if (++openCount == 1) { - if (state == STATE_ERROR) { - return; - } - if (openInternal(true)) { - doLicense(true); - } - } - } - - // Assigning null to various non-null variables for clean-up. Class won't be used after release. - @SuppressWarnings("assignment.type.incompatible") - public void release() { - if (--openCount == 0) { - state = STATE_RELEASED; - postResponseHandler.removeCallbacksAndMessages(null); - postRequestHandler.removeCallbacksAndMessages(null); - postRequestHandler = null; - requestHandlerThread.quit(); - requestHandlerThread = null; - mediaCrypto = null; - lastException = null; - currentKeyRequest = null; - currentProvisionRequest = null; - if (sessionId != null) { - mediaDrm.closeSession(sessionId); - sessionId = null; - eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased); - } - releaseCallback.onSessionReleased(this); - } } public boolean hasSessionId(byte[] sessionId) { @@ -227,7 +188,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; public void provision() { currentProvisionRequest = mediaDrm.getProvisionRequest(); - postRequestHandler.post(MSG_PROVISION, currentProvisionRequest, /* allowRetry= */ true); + Util.castNonNull(postRequestHandler) + .post( + MSG_PROVISION, + Assertions.checkNotNull(currentProvisionRequest), + /* allowRetry= */ true); } public void onProvisionCompleted() { @@ -259,15 +224,53 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public @Nullable Map queryKeyStatus() { + @Nullable + public Map queryKeyStatus() { return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId); } @Override - public @Nullable byte[] getOfflineLicenseKeySetId() { + @Nullable + public byte[] getOfflineLicenseKeySetId() { return offlineLicenseKeySetId; } + @Override + public void acquireReference() { + if (++referenceCount == 1) { + Assertions.checkState(state == STATE_OPENING); + requestHandlerThread = new HandlerThread("DrmRequestHandler"); + requestHandlerThread.start(); + postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); + if (openInternal(true)) { + doLicense(true); + } + } + } + + @Override + public void releaseReference() { + if (--referenceCount == 0) { + // Assigning null to various non-null variables for clean-up. + state = STATE_RELEASED; + Util.castNonNull(postResponseHandler).removeCallbacksAndMessages(null); + Util.castNonNull(postRequestHandler).removeCallbacksAndMessages(null); + postRequestHandler = null; + Util.castNonNull(requestHandlerThread).quit(); + requestHandlerThread = null; + mediaCrypto = null; + lastException = null; + currentKeyRequest = null; + currentProvisionRequest = null; + if (sessionId != null) { + mediaDrm.closeSession(sessionId); + sessionId = null; + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased); + } + releaseCallback.onSessionReleased(this); + } + } + // Internal methods. /** @@ -286,9 +289,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; try { sessionId = mediaDrm.openSession(); - eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired); mediaCrypto = mediaDrm.createMediaCrypto(sessionId); + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired); state = STATE_OPENED; + Assertions.checkNotNull(sessionId); return true; } catch (NotProvisionedException e) { if (allowProvisioning) { @@ -327,6 +331,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @RequiresNonNull("sessionId") private void doLicense(boolean allowRetry) { + byte[] sessionId = Util.castNonNull(this.sessionId); switch (mode) { case DefaultDrmSessionManager.MODE_PLAYBACK: case DefaultDrmSessionManager.MODE_QUERY: @@ -335,7 +340,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } else if (state == STATE_OPENED_WITH_KEYS || restoreKeys()) { long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); if (mode == DefaultDrmSessionManager.MODE_PLAYBACK - && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { + && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW_SECONDS) { Log.d( TAG, "Offline license has expired or will expire soon. " @@ -362,6 +367,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; break; case DefaultDrmSessionManager.MODE_RELEASE: Assertions.checkNotNull(offlineLicenseKeySetId); + Assertions.checkNotNull(this.sessionId); // It's not necessary to restore the key (and open a session to do that) before releasing it // but this serves as a good sanity/fast-failure check. if (restoreKeys()) { @@ -398,7 +404,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; try { currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, optionalKeyRequestParameters); - postRequestHandler.post(MSG_KEYS, currentKeyRequest, allowRetry); + Util.castNonNull(postRequestHandler) + .post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry); } catch (Exception e) { onKeysError(e); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index f27fefa055..34fd223c64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -88,7 +88,7 @@ public class DefaultDrmSessionManager private final UUID uuid; private final ExoMediaDrm mediaDrm; private final MediaDrmCallback callback; - private final @Nullable HashMap optionalKeyRequestParameters; + @Nullable private final HashMap optionalKeyRequestParameters; private final EventDispatcher eventDispatcher; private final boolean multiSession; private final int initialDrmRequestRetryCount; @@ -96,9 +96,9 @@ public class DefaultDrmSessionManager private final List> sessions; private final List> provisioningSessions; - private @Nullable Looper playbackLooper; + @Nullable private Looper playbackLooper; private int mode; - private @Nullable byte[] offlineLicenseKeySetId; + @Nullable private byte[] offlineLicenseKeySetId; /* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler; @@ -432,17 +432,14 @@ public class DefaultDrmSessionManager initialDrmRequestRetryCount); sessions.add(session); } - session.acquire(); + session.acquireReference(); return session; } @Override - public void releaseSession(DrmSession session) { - if (session instanceof ErrorStateDrmSession) { - // Do nothing. - return; - } - ((DefaultDrmSession) session).release(); + @Nullable + public Class getExoMediaCryptoType(DrmInitData drmInitData) { + return canAcquireSession(drmInitData) ? mediaDrm.getExoMediaCryptoType() : null; } // ProvisioningManager implementation. @@ -542,7 +539,7 @@ public class DefaultDrmSessionManager @Override public void onEvent( ExoMediaDrm md, - byte[] sessionId, + @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 3b05bd1e41..7cc2231e0f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -87,7 +87,7 @@ public final class DrmInitData implements Comparator, Parcelable { private int hashCode; /** The protection scheme type, or null if not applicable or unknown. */ - public final @Nullable String schemeType; + @Nullable public final String schemeType; /** * Number of {@link SchemeData}s. @@ -152,7 +152,8 @@ public final class DrmInitData implements Comparator, Parcelable { * @return The initialization data for the scheme, or null if the scheme is not supported. */ @Deprecated - public @Nullable SchemeData get(UUID uuid) { + @Nullable + public SchemeData get(UUID uuid) { for (SchemeData schemeData : schemeDatas) { if (schemeData.matches(uuid)) { return schemeData; @@ -286,11 +287,11 @@ public final class DrmInitData implements Comparator, Parcelable { */ private final UUID uuid; /** The URL of the server to which license requests should be made. May be null if unknown. */ - public final @Nullable String licenseServerUrl; + @Nullable public final String licenseServerUrl; /** The mimeType of {@link #data}. */ public final String mimeType; /** The initialization data. May be null for scheme support checks only. */ - public final @Nullable byte[] data; + @Nullable public final byte[] data; /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 392b0734b1..722ab946f0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.drm; import android.media.MediaDrm; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -29,9 +30,26 @@ import java.util.Map; public interface DrmSession { /** - * Wraps the throwable which is the cause of the error state. + * Invokes {@code newSession's} {@link #acquireReference()} and {@code previousSession's} {@link + * #releaseReference()} in that order. Null arguments are ignored. Does nothing if {@code + * previousSession} and {@code newSession} are the same session. */ - class DrmSessionException extends Exception { + static void replaceSessionReferences( + @Nullable DrmSession previousSession, @Nullable DrmSession newSession) { + if (previousSession == newSession) { + // Do nothing. + return; + } + if (newSession != null) { + newSession.acquireReference(); + } + if (previousSession != null) { + previousSession.releaseReference(); + } + } + + /** Wraps the throwable which is the cause of the error state. */ + class DrmSessionException extends IOException { public DrmSessionException(Throwable cause) { super(cause); @@ -110,4 +128,18 @@ public interface DrmSession { */ @Nullable byte[] getOfflineLicenseKeySetId(); + + /** + * Increments the reference count for this session. A non-zero reference count session will keep + * any acquired resources. + */ + void acquireReference(); + + /** + * Decreases by one the reference count for this session. A session that reaches a zero reference + * count will release any resources it holds. + * + *

The session must not be used after its reference count has been reduced to 0. + */ + void releaseReference(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index d8093507a4..9211cec144 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -16,13 +16,68 @@ package com.google.android.exoplayer2.drm; import android.os.Looper; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Manages a DRM session. */ public interface DrmSessionManager { + /** Returns {@link #DUMMY}. */ + @SuppressWarnings("unchecked") + static DrmSessionManager getDummyDrmSessionManager() { + return (DrmSessionManager) DUMMY; + } + + /** {@link DrmSessionManager} that supports no DRM schemes. */ + DrmSessionManager DUMMY = + new DrmSessionManager() { + + @Override + public boolean canAcquireSession(DrmInitData drmInitData) { + return false; + } + + @Override + public DrmSession acquireSession( + Looper playbackLooper, DrmInitData drmInitData) { + return new ErrorStateDrmSession<>( + new DrmSession.DrmSessionException( + new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME))); + } + + @Override + @Nullable + public Class getExoMediaCryptoType(DrmInitData drmInitData) { + return null; + } + }; + + /** Flags that control the handling of DRM protected content. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS}) + @interface Flags {} + + /** + * When this flag is set, clear samples of an encrypted region may be rendered when no keys are + * available. + * + *

Encrypted media may contain clear (un-encrypted) regions. For example a media file may start + * with a short clear region so as to allow playback to begin in parallel with key acquisition. + * When this flag is set, consumers of sample data are permitted to access the clear regions of + * encrypted media files when the associated {@link DrmSession} has not yet obtained the keys + * necessary for the encrypted regions of the media. + */ + int FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS = 1; + /** * Returns whether the manager is capable of acquiring a session for the given * {@link DrmInitData}. @@ -34,8 +89,10 @@ public interface DrmSessionManager { boolean canAcquireSession(DrmInitData drmInitData); /** - * Acquires a {@link DrmSession} for the specified {@link DrmInitData}. The {@link DrmSession} - * must be returned to {@link #releaseSession(DrmSession)} when it is no longer required. + * Returns a {@link DrmSession} with an acquired reference for the specified {@link DrmInitData}. + * + *

The caller must call {@link DrmSession#releaseReference} to decrement the session's + * reference count when the session is no longer required. * * @param playbackLooper The looper associated with the media playback thread. * @param drmInitData DRM initialization data. All contained {@link SchemeData}s must contain @@ -44,9 +101,16 @@ public interface DrmSessionManager { */ DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData); - /** - * Releases a {@link DrmSession}. - */ - void releaseSession(DrmSession drmSession); + /** Returns flags that control the handling of DRM protected content. */ + @Flags + default int getFlags() { + return 0; + } + /** + * Returns the {@link ExoMediaCrypto} type returned by sessions acquired using the given {@link + * DrmInitData}, or null if a session cannot be acquired with the given {@link DrmInitData}. + */ + @Nullable + Class getExoMediaCryptoType(DrmInitData drmInitData); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java index 82fd9a5549..d40cf60906 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java @@ -34,23 +34,36 @@ public final class ErrorStateDrmSession implements Drm } @Override - public @Nullable DrmSessionException getError() { + @Nullable + public DrmSessionException getError() { return error; } @Override - public @Nullable T getMediaCrypto() { + @Nullable + public T getMediaCrypto() { return null; } @Override - public @Nullable Map queryKeyStatus() { + @Nullable + public Map queryKeyStatus() { return null; } @Override - public @Nullable byte[] getOfflineLicenseKeySetId() { + @Nullable + public byte[] getOfflineLicenseKeySetId() { return null; } + @Override + public void acquireReference() { + // Do nothing. + } + + @Override + public void releaseReference() { + // Do nothing. + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 49915f3af5..ca776267aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -80,7 +80,7 @@ public interface ExoMediaDrm { */ void onEvent( ExoMediaDrm mediaDrm, - byte[] sessionId, + @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data); @@ -215,6 +215,7 @@ public interface ExoMediaDrm { throws NotProvisionedException; /** @see MediaDrm#provideKeyResponse(byte[], byte[]) */ + @Nullable byte[] provideKeyResponse(byte[] scope, byte[] response) throws NotProvisionedException, DeniedByServerException; @@ -270,4 +271,7 @@ public interface ExoMediaDrm { * @throws MediaCryptoException If the instance can't be created. */ T createMediaCrypto(byte[] sessionId) throws MediaCryptoException; + + /** Returns the {@link ExoMediaCrypto} type created by {@link #createMediaCrypto(byte[])}. */ + Class getExoMediaCryptoType(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index 848d9e146a..e77504c91c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -84,8 +84,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { @@ -160,8 +158,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm getExoMediaCryptoType() { + return FrameworkMediaCrypto.class; + } + private static SchemeData getSchemeData(UUID uuid, List schemeDatas) { if (!C.WIDEVINE_UUID.equals(uuid)) { // For non-Widevine CDMs always use the first scheme data. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index a3e602e404..23b2300dfa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -111,7 +111,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { String url = request.getDefaultUrl() + "&signedRequest=" + Util.fromUtf8Bytes(request.getData()); - return executePost(dataSourceFactory, url, Util.EMPTY_BYTE_ARRAY, null); + return executePost(dataSourceFactory, url, /* httpBody= */ null, /* requestProperties= */ null); } @Override @@ -139,7 +139,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { private static byte[] executePost( HttpDataSource.Factory dataSourceFactory, String url, - byte[] data, + @Nullable byte[] httpBody, @Nullable Map requestProperties) throws IOException { HttpDataSource dataSource = dataSourceFactory.createDataSource(); @@ -154,7 +154,8 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { DataSpec dataSpec = new DataSpec( Uri.parse(url), - data, + DataSpec.HTTP_METHOD_POST, + httpBody, /* absoluteStreamPosition= */ 0, /* position= */ 0, /* length= */ C.LENGTH_UNSET, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 55a7a901ac..05dab7e42d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -235,7 +235,7 @@ public final class OfflineLicenseHelper { DrmSessionException error = drmSession.getError(); Pair licenseDurationRemainingSec = WidevineUtil.getLicenseDurationRemainingSec(drmSession); - drmSessionManager.releaseSession(drmSession); + drmSession.releaseReference(); if (error != null) { if (error.getCause() instanceof KeysExpiredException) { return Pair.create(0L, 0L); @@ -259,7 +259,7 @@ public final class OfflineLicenseHelper { drmInitData); DrmSessionException error = drmSession.getError(); byte[] keySetId = drmSession.getOfflineLicenseKeySetId(); - drmSessionManager.releaseSession(drmSession); + drmSession.releaseReference(); if (error != null) { throw error; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 54c78eb33d..02c676dfdf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -108,7 +108,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { /** * Sets flags for {@link AdtsExtractor} instances created by the factory. * - * @see AdtsExtractor#AdtsExtractor(long, int) + * @see AdtsExtractor#AdtsExtractor(int) * @param flags The flags to use. * @return The factory, for convenience. */ @@ -220,7 +220,6 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { : 0)); extractors[4] = new AdtsExtractor( - /* firstStreamSampleTimestampUs= */ 0, adtsFlags | (constantBitrateSeekingEnabled ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java index 255799c026..60386dcc3c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java @@ -53,7 +53,7 @@ public final class Id3Peeker { Metadata metadata = null; while (true) { try { - input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH); + input.peekFully(scratch.data, /* offset= */ 0, Id3Decoder.ID3_HEADER_LENGTH); } catch (EOFException e) { // If input has less than ID3_HEADER_LENGTH, ignore the rest. break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java index 87bb992082..e454bd51c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java @@ -186,10 +186,6 @@ public final class MpegAudioHeader { } } - // Calculate the bitrate in the same way Mp3Extractor calculates sample timestamps so that - // seeking to a given timestamp and playing from the start up to that timestamp give the same - // results for CBR streams. See also [internal: b/120390268]. - bitrate = 8 * frameSize * sampleRate / samplesPerFrame; String mimeType = MIME_TYPE_BY_LAYER[3 - layer]; int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java index d7a1c75302..0d5a168197 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java @@ -119,7 +119,7 @@ public interface TrackOutput { * Called to write sample data to the output. * * @param data A {@link ParsableByteArray} from which to read the sample data. - * @param length The number of bytes to read. + * @param length The number of bytes to read, starting from {@code data.getPosition()}. */ void sampleData(ParsableByteArray data, int length); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java index caf12948ad..f6b64245fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java @@ -140,7 +140,7 @@ public final class AmrExtractor implements Extractor { private ExtractorOutput extractorOutput; private TrackOutput trackOutput; - private @Nullable SeekMap seekMap; + @Nullable private SeekMap seekMap; private boolean hasOutputFormat; public AmrExtractor() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index ec5ad88aeb..b10f2bf80b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -86,11 +86,12 @@ import java.util.Collections; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { if (audioFormat == AUDIO_FORMAT_MP3) { int sampleSize = data.bytesLeft(); output.sampleData(data, sampleSize); output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + return true; } else { int packetType = data.readUnsignedByte(); if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { @@ -104,12 +105,15 @@ import java.util.Collections; Collections.singletonList(audioSpecificConfig), null, 0, null); output.format(format); hasOutputFormat = true; + return false; } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) { int sampleSize = data.bytesLeft(); output.sampleData(data, sampleSize); output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + return true; + } else { + return false; } } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 0a2c0c46f6..f6835558f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -64,7 +63,7 @@ public final class FlvExtractor implements Extractor { private static final int TAG_TYPE_SCRIPT_DATA = 18; // FLV container identifier. - private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); + private static final int FLV_TAG = 0x00464c56; private final ParsableByteArray scratch; private final ParsableByteArray headerBuffer; @@ -74,6 +73,7 @@ public final class FlvExtractor implements Extractor { private ExtractorOutput extractorOutput; private @States int state; + private boolean outputFirstSample; private long mediaTagTimestampOffsetUs; private int bytesToNextTagHeader; private int tagType; @@ -90,7 +90,6 @@ public final class FlvExtractor implements Extractor { tagData = new ParsableByteArray(); metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; - mediaTagTimestampOffsetUs = C.TIME_UNSET; } @Override @@ -132,7 +131,7 @@ public final class FlvExtractor implements Extractor { @Override public void seek(long position, long timeUs) { state = STATE_READING_FLV_HEADER; - mediaTagTimestampOffsetUs = C.TIME_UNSET; + outputFirstSample = false; bytesToNextTagHeader = 0; } @@ -253,14 +252,16 @@ public final class FlvExtractor implements Extractor { */ private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; + boolean wasSampleOutput = false; + long timestampUs = getCurrentTimestampUs(); if (tagType == TAG_TYPE_AUDIO && audioReader != null) { ensureReadyForMediaOutput(); - audioReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); + wasSampleOutput = audioReader.consume(prepareTagData(input), timestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { ensureReadyForMediaOutput(); - videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); + wasSampleOutput = videoReader.consume(prepareTagData(input), timestampUs); } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { - metadataReader.consume(prepareTagData(input), tagTimestampUs); + wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs); long durationUs = metadataReader.getDurationUs(); if (durationUs != C.TIME_UNSET) { extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); @@ -270,6 +271,11 @@ public final class FlvExtractor implements Extractor { input.skipFully(tagDataSize); wasConsumed = false; } + if (!outputFirstSample && wasSampleOutput) { + outputFirstSample = true; + mediaTagTimestampOffsetUs = + metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; + } bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. state = STATE_SKIPPING_TO_TAG_HEADER; return wasConsumed; @@ -292,10 +298,11 @@ public final class FlvExtractor implements Extractor { extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); outputSeekMap = true; } - if (mediaTagTimestampOffsetUs == C.TIME_UNSET) { - mediaTagTimestampOffsetUs = - metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; - } } + private long getCurrentTimestampUs() { + return outputFirstSample + ? (mediaTagTimestampOffsetUs + tagTimestampUs) + : (metadataReader.getDurationUs() == C.TIME_UNSET ? 0 : tagTimestampUs); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index 2dec85ffcc..806cc9fad4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.extractor.flv; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Date; @@ -44,7 +46,7 @@ import java.util.Map; private long durationUs; public ScriptTagPayloadReader() { - super(null); + super(new DummyTrackOutput()); durationUs = C.TIME_UNSET; } @@ -63,7 +65,7 @@ import java.util.Map; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int nameType = readAmfType(data); if (nameType != AMF_TYPE_STRING) { // Should never happen. @@ -72,12 +74,12 @@ import java.util.Map; String name = readAmfString(data); if (!NAME_METADATA.equals(name)) { // We're only interested in metadata. - return; + return false; } int type = readAmfType(data); if (type != AMF_TYPE_ECMA_ARRAY) { // We're not interested in this metadata. - return; + return false; } // Set the duration to the value contained in the metadata, if present. Map metadata = readAmfEcmaArray(data); @@ -87,6 +89,7 @@ import java.util.Map; durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND); } } + return false; } private static int readAmfType(ParsableByteArray data) { @@ -137,7 +140,10 @@ import java.util.Map; ArrayList list = new ArrayList<>(count); for (int i = 0; i < count; i++) { int type = readAmfType(data); - list.add(readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + list.add(value); + } } return list; } @@ -156,7 +162,10 @@ import java.util.Map; if (type == AMF_TYPE_END_MARKER) { break; } - array.put(key, readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } } return array; } @@ -173,7 +182,10 @@ import java.util.Map; for (int i = 0; i < count; i++) { String key = readAmfString(data); int type = readAmfType(data); - array.put(key, readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } } return array; } @@ -190,6 +202,7 @@ import java.util.Map; return date; } + @Nullable private static Object readAmfData(ParsableByteArray data, int type) { switch (type) { case AMF_TYPE_NUMBER: @@ -207,8 +220,8 @@ import java.util.Map; case AMF_TYPE_DATE: return readAmfDate(data); default: + // We don't log a warning because there are types that we knowingly don't support. return null; } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java index e8652d653f..48914b7c2c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java @@ -58,12 +58,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; * * @param data The payload data to consume. * @param timeUs The timestamp associated with the payload. + * @return Whether a sample was output. * @throws ParserException If an error occurs parsing the data. */ - public final void consume(ParsableByteArray data, long timeUs) throws ParserException { - if (parseHeader(data)) { - parsePayload(data, timeUs); - } + public final boolean consume(ParsableByteArray data, long timeUs) throws ParserException { + return parseHeader(data) && parsePayload(data, timeUs); } /** @@ -78,10 +77,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses tag payload. * - * @param data Buffer where tag payload is stored - * @param timeUs Time position of the frame + * @param data Buffer where tag payload is stored. + * @param timeUs Time position of the frame. + * @return Whether a sample was output. * @throws ParserException If an error occurs parsing the payload. */ - protected abstract void parsePayload(ParsableByteArray data, long timeUs) throws ParserException; - + protected abstract boolean parsePayload(ParsableByteArray data, long timeUs) + throws ParserException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java index 92db91e20b..5ddaafb4a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java @@ -47,6 +47,7 @@ import com.google.android.exoplayer2.video.AvcConfig; // State variables. private boolean hasOutputFormat; + private boolean hasOutputKeyframe; private int frameType; /** @@ -60,7 +61,7 @@ import com.google.android.exoplayer2.video.AvcConfig; @Override public void seek() { - // Do nothing. + hasOutputKeyframe = false; } @Override @@ -77,7 +78,7 @@ import com.google.android.exoplayer2.video.AvcConfig; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int packetType = data.readUnsignedByte(); int compositionTimeMs = data.readInt24(); @@ -94,7 +95,12 @@ import com.google.android.exoplayer2.video.AvcConfig; avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null); output.format(format); hasOutputFormat = true; + return false; } else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) { + boolean isKeyframe = frameType == VIDEO_FRAME_KEYFRAME; + if (!hasOutputKeyframe && !isKeyframe) { + return false; + } // TODO: Deduplicate with Mp4Extractor. // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. @@ -123,8 +129,12 @@ import com.google.android.exoplayer2.video.AvcConfig; output.sampleData(data, bytesToWrite); bytesWritten += bytesToWrite; } - output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.BUFFER_FLAG_KEY_FRAME : 0, - bytesWritten, 0, null); + output.sampleMetadata( + timeUs, isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0, bytesWritten, 0, null); + hasOutputKeyframe = true; + return true; + } else { + return false; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index c65ad0bc67..ecff963271 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import com.google.android.exoplayer2.metadata.id3.MlltFrame; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Documented; @@ -95,9 +94,9 @@ public final class Mp3Extractor implements Extractor { */ private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00; - private static final int SEEK_HEADER_XING = Util.getIntegerCodeForString("Xing"); - private static final int SEEK_HEADER_INFO = Util.getIntegerCodeForString("Info"); - private static final int SEEK_HEADER_VBRI = Util.getIntegerCodeForString("VBRI"); + private static final int SEEK_HEADER_XING = 0x58696e67; + private static final int SEEK_HEADER_INFO = 0x496e666f; + private static final int SEEK_HEADER_VBRI = 0x56425249; private static final int SEEK_HEADER_UNSET = 0; @Flags private final int flags; @@ -117,6 +116,7 @@ public final class Mp3Extractor implements Extractor { private Seeker seeker; private long basisTimeUs; private long samplesRead; + private long firstSamplePosition; private int sampleBytesRemaining; public Mp3Extractor() { @@ -214,6 +214,13 @@ public final class Mp3Extractor implements Extractor { /* selectionFlags= */ 0, /* language= */ null, (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); + firstSamplePosition = input.getPosition(); + } else if (firstSamplePosition != 0) { + long inputPosition = input.getPosition(); + if (inputPosition < firstSamplePosition) { + // Skip past the seek frame. + input.skipFully((int) (firstSamplePosition - inputPosition)); + } } return readSample(input); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index 116a123094..c0c2080e17 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -90,7 +90,7 @@ import com.google.android.exoplayer2.util.Util; * Entries are in the range [0, 255], but are stored as long integers for convenience. Null if the * table of contents was missing from the header, in which case seeking is not be supported. */ - private final @Nullable long[] tableOfContents; + @Nullable private final long[] tableOfContents; private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) { this( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index f66c1f5d2c..572efed1af 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.mp4; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -51,334 +50,334 @@ import java.util.List; public static final int EXTENDS_TO_END_SIZE = 0; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp"); + public static final int TYPE_ftyp = 0x66747970; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1"); + public static final int TYPE_avc1 = 0x61766331; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3"); + public static final int TYPE_avc3 = 0x61766333; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC"); + public static final int TYPE_avcC = 0x61766343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hvc1 = Util.getIntegerCodeForString("hvc1"); + public static final int TYPE_hvc1 = 0x68766331; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hev1 = Util.getIntegerCodeForString("hev1"); + public static final int TYPE_hev1 = 0x68657631; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hvcC = Util.getIntegerCodeForString("hvcC"); + public static final int TYPE_hvcC = 0x68766343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08"); + public static final int TYPE_vp08 = 0x76703038; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); + public static final int TYPE_vp09 = 0x76703039; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); + public static final int TYPE_vpcC = 0x76706343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_av01 = Util.getIntegerCodeForString("av01"); + public static final int TYPE_av01 = 0x61763031; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_av1C = Util.getIntegerCodeForString("av1C"); + public static final int TYPE_av1C = 0x61763143; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvav = Util.getIntegerCodeForString("dvav"); + public static final int TYPE_dvav = 0x64766176; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dva1 = Util.getIntegerCodeForString("dva1"); + public static final int TYPE_dva1 = 0x64766131; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvhe = Util.getIntegerCodeForString("dvhe"); + public static final int TYPE_dvhe = 0x64766865; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvh1 = Util.getIntegerCodeForString("dvh1"); + public static final int TYPE_dvh1 = 0x64766831; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvcC = Util.getIntegerCodeForString("dvcC"); + public static final int TYPE_dvcC = 0x64766343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvvC = Util.getIntegerCodeForString("dvvC"); + public static final int TYPE_dvvC = 0x64767643; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_s263 = Util.getIntegerCodeForString("s263"); + public static final int TYPE_s263 = 0x73323633; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_d263 = Util.getIntegerCodeForString("d263"); + public static final int TYPE_d263 = 0x64323633; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat"); + public static final int TYPE_mdat = 0x6d646174; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a"); + public static final int TYPE_mp4a = 0x6d703461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE__mp3 = Util.getIntegerCodeForString(".mp3"); + public static final int TYPE__mp3 = 0x2e6d7033; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_wave = Util.getIntegerCodeForString("wave"); + public static final int TYPE_wave = 0x77617665; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_lpcm = Util.getIntegerCodeForString("lpcm"); + public static final int TYPE_lpcm = 0x6c70636d; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sowt = Util.getIntegerCodeForString("sowt"); + public static final int TYPE_sowt = 0x736f7774; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3"); + public static final int TYPE_ac_3 = 0x61632d33; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dac3 = Util.getIntegerCodeForString("dac3"); + public static final int TYPE_dac3 = 0x64616333; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ec_3 = Util.getIntegerCodeForString("ec-3"); + public static final int TYPE_ec_3 = 0x65632d33; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dec3 = Util.getIntegerCodeForString("dec3"); + public static final int TYPE_dec3 = 0x64656333; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ac_4 = Util.getIntegerCodeForString("ac-4"); + public static final int TYPE_ac_4 = 0x61632d34; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dac4 = Util.getIntegerCodeForString("dac4"); + public static final int TYPE_dac4 = 0x64616334; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtsc = Util.getIntegerCodeForString("dtsc"); + public static final int TYPE_dtsc = 0x64747363; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtsh = Util.getIntegerCodeForString("dtsh"); + public static final int TYPE_dtsh = 0x64747368; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtsl = Util.getIntegerCodeForString("dtsl"); + public static final int TYPE_dtsl = 0x6474736c; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtse = Util.getIntegerCodeForString("dtse"); + public static final int TYPE_dtse = 0x64747365; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ddts = Util.getIntegerCodeForString("ddts"); + public static final int TYPE_ddts = 0x64647473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tfdt = Util.getIntegerCodeForString("tfdt"); + public static final int TYPE_tfdt = 0x74666474; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tfhd = Util.getIntegerCodeForString("tfhd"); + public static final int TYPE_tfhd = 0x74666864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_trex = Util.getIntegerCodeForString("trex"); + public static final int TYPE_trex = 0x74726578; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_trun = Util.getIntegerCodeForString("trun"); + public static final int TYPE_trun = 0x7472756e; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sidx = Util.getIntegerCodeForString("sidx"); + public static final int TYPE_sidx = 0x73696478; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_moov = Util.getIntegerCodeForString("moov"); + public static final int TYPE_moov = 0x6d6f6f76; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mvhd = Util.getIntegerCodeForString("mvhd"); + public static final int TYPE_mvhd = 0x6d766864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_trak = Util.getIntegerCodeForString("trak"); + public static final int TYPE_trak = 0x7472616b; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mdia = Util.getIntegerCodeForString("mdia"); + public static final int TYPE_mdia = 0x6d646961; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_minf = Util.getIntegerCodeForString("minf"); + public static final int TYPE_minf = 0x6d696e66; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl"); + public static final int TYPE_stbl = 0x7374626c; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_esds = Util.getIntegerCodeForString("esds"); + public static final int TYPE_esds = 0x65736473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_moof = Util.getIntegerCodeForString("moof"); + public static final int TYPE_moof = 0x6d6f6f66; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_traf = Util.getIntegerCodeForString("traf"); + public static final int TYPE_traf = 0x74726166; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex"); + public static final int TYPE_mvex = 0x6d766578; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mehd = Util.getIntegerCodeForString("mehd"); + public static final int TYPE_mehd = 0x6d656864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tkhd = Util.getIntegerCodeForString("tkhd"); + public static final int TYPE_tkhd = 0x746b6864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_edts = Util.getIntegerCodeForString("edts"); + public static final int TYPE_edts = 0x65647473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_elst = Util.getIntegerCodeForString("elst"); + public static final int TYPE_elst = 0x656c7374; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mdhd = Util.getIntegerCodeForString("mdhd"); + public static final int TYPE_mdhd = 0x6d646864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hdlr = Util.getIntegerCodeForString("hdlr"); + public static final int TYPE_hdlr = 0x68646c72; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stsd = Util.getIntegerCodeForString("stsd"); + public static final int TYPE_stsd = 0x73747364; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_pssh = Util.getIntegerCodeForString("pssh"); + public static final int TYPE_pssh = 0x70737368; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sinf = Util.getIntegerCodeForString("sinf"); + public static final int TYPE_sinf = 0x73696e66; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_schm = Util.getIntegerCodeForString("schm"); + public static final int TYPE_schm = 0x7363686d; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_schi = Util.getIntegerCodeForString("schi"); + public static final int TYPE_schi = 0x73636869; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tenc = Util.getIntegerCodeForString("tenc"); + public static final int TYPE_tenc = 0x74656e63; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_encv = Util.getIntegerCodeForString("encv"); + public static final int TYPE_encv = 0x656e6376; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_enca = Util.getIntegerCodeForString("enca"); + public static final int TYPE_enca = 0x656e6361; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_frma = Util.getIntegerCodeForString("frma"); + public static final int TYPE_frma = 0x66726d61; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_saiz = Util.getIntegerCodeForString("saiz"); + public static final int TYPE_saiz = 0x7361697a; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_saio = Util.getIntegerCodeForString("saio"); + public static final int TYPE_saio = 0x7361696f; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sbgp = Util.getIntegerCodeForString("sbgp"); + public static final int TYPE_sbgp = 0x73626770; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sgpd = Util.getIntegerCodeForString("sgpd"); + public static final int TYPE_sgpd = 0x73677064; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_uuid = Util.getIntegerCodeForString("uuid"); + public static final int TYPE_uuid = 0x75756964; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_senc = Util.getIntegerCodeForString("senc"); + public static final int TYPE_senc = 0x73656e63; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_pasp = Util.getIntegerCodeForString("pasp"); + public static final int TYPE_pasp = 0x70617370; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_TTML = Util.getIntegerCodeForString("TTML"); + public static final int TYPE_TTML = 0x54544d4c; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vmhd = Util.getIntegerCodeForString("vmhd"); + public static final int TYPE_vmhd = 0x766d6864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mp4v = Util.getIntegerCodeForString("mp4v"); + public static final int TYPE_mp4v = 0x6d703476; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stts = Util.getIntegerCodeForString("stts"); + public static final int TYPE_stts = 0x73747473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stss = Util.getIntegerCodeForString("stss"); + public static final int TYPE_stss = 0x73747373; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ctts = Util.getIntegerCodeForString("ctts"); + public static final int TYPE_ctts = 0x63747473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stsc = Util.getIntegerCodeForString("stsc"); + public static final int TYPE_stsc = 0x73747363; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stsz = Util.getIntegerCodeForString("stsz"); + public static final int TYPE_stsz = 0x7374737a; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stz2 = Util.getIntegerCodeForString("stz2"); + public static final int TYPE_stz2 = 0x73747a32; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stco = Util.getIntegerCodeForString("stco"); + public static final int TYPE_stco = 0x7374636f; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_co64 = Util.getIntegerCodeForString("co64"); + public static final int TYPE_co64 = 0x636f3634; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g"); + public static final int TYPE_tx3g = 0x74783367; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_wvtt = Util.getIntegerCodeForString("wvtt"); + public static final int TYPE_wvtt = 0x77767474; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp"); + public static final int TYPE_stpp = 0x73747070; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_c608 = Util.getIntegerCodeForString("c608"); + public static final int TYPE_c608 = 0x63363038; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_samr = Util.getIntegerCodeForString("samr"); + public static final int TYPE_samr = 0x73616d72; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb"); + public static final int TYPE_sawb = 0x73617762; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_udta = Util.getIntegerCodeForString("udta"); + public static final int TYPE_udta = 0x75647461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_meta = Util.getIntegerCodeForString("meta"); + public static final int TYPE_meta = 0x6d657461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_keys = Util.getIntegerCodeForString("keys"); + public static final int TYPE_keys = 0x6b657973; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst"); + public static final int TYPE_ilst = 0x696c7374; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mean = Util.getIntegerCodeForString("mean"); + public static final int TYPE_mean = 0x6d65616e; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_name = Util.getIntegerCodeForString("name"); + public static final int TYPE_name = 0x6e616d65; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_data = Util.getIntegerCodeForString("data"); + public static final int TYPE_data = 0x64617461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_emsg = Util.getIntegerCodeForString("emsg"); + public static final int TYPE_emsg = 0x656d7367; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d"); + public static final int TYPE_st3d = 0x73743364; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d"); + public static final int TYPE_sv3d = 0x73763364; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_proj = Util.getIntegerCodeForString("proj"); + public static final int TYPE_proj = 0x70726f6a; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_camm = Util.getIntegerCodeForString("camm"); + public static final int TYPE_camm = 0x63616d6d; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_alac = Util.getIntegerCodeForString("alac"); + public static final int TYPE_alac = 0x616c6163; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_alaw = Util.getIntegerCodeForString("alaw"); + public static final int TYPE_alaw = 0x616c6177; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ulaw = Util.getIntegerCodeForString("ulaw"); + public static final int TYPE_ulaw = 0x756c6177; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_Opus = Util.getIntegerCodeForString("Opus"); + public static final int TYPE_Opus = 0x4f707573; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dOps = Util.getIntegerCodeForString("dOps"); + public static final int TYPE_dOps = 0x644f7073; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_fLaC = Util.getIntegerCodeForString("fLaC"); + public static final int TYPE_fLaC = 0x664c6143; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dfLa = Util.getIntegerCodeForString("dfLa"); + public static final int TYPE_dfLa = 0x64664c61; public final int type; @@ -459,7 +458,8 @@ import java.util.List; * @param type The leaf type. * @return The child leaf of the given type, or null if no such child exists. */ - public @Nullable LeafAtom getLeafAtomOfType(int type) { + @Nullable + public LeafAtom getLeafAtomOfType(int type) { int childrenSize = leafChildren.size(); for (int i = 0; i < childrenSize; i++) { LeafAtom atom = leafChildren.get(i); @@ -479,7 +479,8 @@ import java.util.List; * @param type The container type. * @return The child container of the given type, or null if no such child exists. */ - public @Nullable ContainerAtom getContainerAtomOfType(int type) { + @Nullable + public ContainerAtom getContainerAtomOfType(int type) { int childrenSize = containerChildren.size(); for (int i = 0; i < childrenSize; i++) { ContainerAtom atom = containerChildren.get(i); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 3b74240379..b3c26246e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -42,19 +42,34 @@ import java.util.Collections; import java.util.List; /** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ -@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"}) +@SuppressWarnings({"ConstantField"}) /* package */ final class AtomParsers { private static final String TAG = "AtomParsers"; - private static final int TYPE_vide = Util.getIntegerCodeForString("vide"); - private static final int TYPE_soun = Util.getIntegerCodeForString("soun"); - private static final int TYPE_text = Util.getIntegerCodeForString("text"); - private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); - private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); - private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); - private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); - private static final int TYPE_mdta = Util.getIntegerCodeForString("mdta"); + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_vide = 0x76696465; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_soun = 0x736f756e; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_text = 0x74657874; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_sbtl = 0x7362746c; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_subt = 0x73756274; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_clcp = 0x636c6370; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_meta = 0x6d657461; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_mdta = 0x6d647461; /** * The threshold number of samples to trim from the start/end of an audio track when applying an @@ -1139,10 +1154,6 @@ import java.util.List; out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); - } else if (childAtomType == Atom.TYPE_alac) { - initializationData = new byte[childAtomSize]; - parent.setPosition(childPosition); - parent.readBytes(initializationData, /* offset= */ 0, childAtomSize); } else if (childAtomType == Atom.TYPE_dOps) { // Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic // Signature and the body of the dOps atom. @@ -1151,7 +1162,7 @@ import java.util.List; System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length); parent.setPosition(childPosition + Atom.HEADER_SIZE); parent.readBytes(initializationData, opusMagic.length, childAtomBodySize); - } else if (childAtomSize == Atom.TYPE_dfLa) { + } else if (childAtomSize == Atom.TYPE_dfLa || childAtomType == Atom.TYPE_alac) { int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; initializationData = new byte[childAtomBodySize]; parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 4f45e85762..5eaa5d5d31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -35,6 +35,8 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; import com.google.android.exoplayer2.text.cea.CeaUtil; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -55,6 +57,7 @@ import java.util.List; import java.util.UUID; /** Extracts data from the FMP4 container format. */ +@SuppressWarnings("ConstantField") public class FragmentedMp4Extractor implements Extractor { /** Factory for {@link FragmentedMp4Extractor} instances. */ @@ -104,8 +107,8 @@ public class FragmentedMp4Extractor implements Extractor { private static final String TAG = "FragmentedMp4Extractor"; - @SuppressWarnings("ConstantField") - private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); + @SuppressWarnings("ConstantCaseForConstants") + private static final int SAMPLE_GROUP_TYPE_seig = 0x73656967; private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; @@ -121,11 +124,11 @@ public class FragmentedMp4Extractor implements Extractor { // Workarounds. @Flags private final int flags; - private final @Nullable Track sideloadedTrack; + @Nullable private final Track sideloadedTrack; // Sideloaded data. private final List closedCaptionFormats; - private final @Nullable DrmInitData sideloadedDrmInitData; + @Nullable private final DrmInitData sideloadedDrmInitData; // Track-linked data bundle, accessible as a whole through trackID. private final SparseArray trackBundles; @@ -138,13 +141,15 @@ public class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray scratch; // Adjusts sample timestamps. - private final @Nullable TimestampAdjuster timestampAdjuster; + @Nullable private final TimestampAdjuster timestampAdjuster; + + private final EventMessageEncoder eventMessageEncoder; // Parser state. private final ParsableByteArray atomHeader; private final ArrayDeque containerAtoms; private final ArrayDeque pendingMetadataSampleInfos; - private final @Nullable TrackOutput additionalEmsgTrackOutput; + @Nullable private final TrackOutput additionalEmsgTrackOutput; private int parserState; private int atomType; @@ -253,6 +258,7 @@ public class FragmentedMp4Extractor implements Extractor { this.sideloadedDrmInitData = sideloadedDrmInitData; this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; + eventMessageEncoder = new EventMessageEncoder(); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); @@ -590,39 +596,71 @@ public class FragmentedMp4Extractor implements Extractor { } } - /** - * Parses an emsg atom (defined in 23009-1). - */ + /** Handles an emsg atom (defined in 23009-1). */ private void onEmsgLeafAtomRead(ParsableByteArray atom) { if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) { return; } + atom.setPosition(Atom.HEADER_SIZE); + int fullAtom = atom.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + String schemeIdUri; + String value; + long timescale; + long presentationTimeDeltaUs = C.TIME_UNSET; // Only set if version == 0 + long sampleTimeUs = C.TIME_UNSET; + long durationMs; + long id; + switch (version) { + case 0: + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + timescale = atom.readUnsignedInt(); + presentationTimeDeltaUs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); + if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { + sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs; + } + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + break; + case 1: + timescale = atom.readUnsignedInt(); + sampleTimeUs = + Util.scaleLargeTimestamp(atom.readUnsignedLongToLong(), C.MICROS_PER_SECOND, timescale); + durationMs = + Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MILLIS_PER_SECOND, timescale); + id = atom.readUnsignedInt(); + schemeIdUri = Assertions.checkNotNull(atom.readNullTerminatedString()); + value = Assertions.checkNotNull(atom.readNullTerminatedString()); + break; + default: + Log.w(TAG, "Skipping unsupported emsg version: " + version); + return; + } - atom.setPosition(Atom.FULL_HEADER_SIZE); - int sampleSize = atom.bytesLeft(); - atom.readNullTerminatedString(); // schemeIdUri - atom.readNullTerminatedString(); // value - long timescale = atom.readUnsignedInt(); - long presentationTimeDeltaUs = - Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); - - // The presentation_time_delta is accounted for by adjusting the sample timestamp, so we zero it - // in the sample data before writing it to the track outputs. - int position = atom.getPosition(); - atom.data[position - 4] = 0; - atom.data[position - 3] = 0; - atom.data[position - 2] = 0; - atom.data[position - 1] = 0; + byte[] messageData = new byte[atom.bytesLeft()]; + atom.readBytes(messageData, /*offset=*/ 0, atom.bytesLeft()); + EventMessage eventMessage = new EventMessage(schemeIdUri, value, durationMs, id, messageData); + ParsableByteArray encodedEventMessage = + new ParsableByteArray(eventMessageEncoder.encode(eventMessage)); + int sampleSize = encodedEventMessage.bytesLeft(); // Output the sample data. for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { - atom.setPosition(Atom.FULL_HEADER_SIZE); - emsgTrackOutput.sampleData(atom, sampleSize); + encodedEventMessage.setPosition(0); + emsgTrackOutput.sampleData(encodedEventMessage, sampleSize); } - // Output the sample metadata. - if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { - long sampleTimeUs = segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs; + // Output the sample metadata. This is made a little complicated because emsg-v0 atoms + // have presentation time *delta* while v1 atoms have absolute presentation time. + if (sampleTimeUs == C.TIME_UNSET) { + // We need the first sample timestamp in the segment before we can output the metadata. + pendingMetadataSampleInfos.addLast( + new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); + pendingMetadataSampleBytes += sampleSize; + } else { if (timestampAdjuster != null) { sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); } @@ -630,17 +668,10 @@ public class FragmentedMp4Extractor implements Extractor { emsgTrackOutput.sampleMetadata( sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, null); } - } else { - // We need the first sample timestamp in the segment before we can output the metadata. - pendingMetadataSampleInfos.addLast( - new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize)); - pendingMetadataSampleBytes += sampleSize; } } - /** - * Parses a trex atom (defined in 14496-12). - */ + /** Parses a trex atom (defined in 14496-12). */ private static Pair parseTrex(ParsableByteArray trex) { trex.setPosition(Atom.FULL_HEADER_SIZE); int trackId = trex.readInt(); @@ -934,7 +965,9 @@ public class FragmentedMp4Extractor implements Extractor { // duration == 0). Other uses of edit lists are uncommon and unsupported. if (track.editListDurations != null && track.editListDurations.length == 1 && track.editListDurations[0] == 0) { - edtsOffset = Util.scaleLargeTimestamp(track.editListMediaTimes[0], 1000, track.timescale); + edtsOffset = + Util.scaleLargeTimestamp( + track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale); } int[] sampleSizeTable = fragment.sampleSizeTable; @@ -962,12 +995,13 @@ public class FragmentedMp4Extractor implements Extractor { // here, because unsigned integers will still be parsed correctly (unless their top bit is // set, which is never true in practice because sample offsets are always small). int sampleOffset = trun.readInt(); - sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000L) / timescale); + sampleCompositionTimeOffsetTable[i] = + (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale); } else { sampleCompositionTimeOffsetTable[i] = 0; } sampleDecodingTimeTable[i] = - Util.scaleLargeTimestamp(cumulativeTime, 1000, timescale) - edtsOffset; + Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset; sampleSizeTable[i] = sampleSize; sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index e9c9f7faf5..bec2cdbb5f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.metadata.id3.InternalFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; /** Utilities for handling metadata in MP4. */ @@ -36,41 +35,41 @@ import java.nio.ByteBuffer; private static final String TAG = "MetadataUtil"; // Codes that start with the copyright character (omitted) and have equivalent ID3 frames. - private static final int SHORT_TYPE_NAME_1 = Util.getIntegerCodeForString("nam"); - private static final int SHORT_TYPE_NAME_2 = Util.getIntegerCodeForString("trk"); - private static final int SHORT_TYPE_COMMENT = Util.getIntegerCodeForString("cmt"); - private static final int SHORT_TYPE_YEAR = Util.getIntegerCodeForString("day"); - private static final int SHORT_TYPE_ARTIST = Util.getIntegerCodeForString("ART"); - private static final int SHORT_TYPE_ENCODER = Util.getIntegerCodeForString("too"); - private static final int SHORT_TYPE_ALBUM = Util.getIntegerCodeForString("alb"); - private static final int SHORT_TYPE_COMPOSER_1 = Util.getIntegerCodeForString("com"); - private static final int SHORT_TYPE_COMPOSER_2 = Util.getIntegerCodeForString("wrt"); - private static final int SHORT_TYPE_LYRICS = Util.getIntegerCodeForString("lyr"); - private static final int SHORT_TYPE_GENRE = Util.getIntegerCodeForString("gen"); + private static final int SHORT_TYPE_NAME_1 = 0x006e616d; + private static final int SHORT_TYPE_NAME_2 = 0x0074726b; + private static final int SHORT_TYPE_COMMENT = 0x00636d74; + private static final int SHORT_TYPE_YEAR = 0x00646179; + private static final int SHORT_TYPE_ARTIST = 0x00415254; + private static final int SHORT_TYPE_ENCODER = 0x00746f6f; + private static final int SHORT_TYPE_ALBUM = 0x00616c62; + private static final int SHORT_TYPE_COMPOSER_1 = 0x00636f6d; + private static final int SHORT_TYPE_COMPOSER_2 = 0x00777274; + private static final int SHORT_TYPE_LYRICS = 0x006c7972; + private static final int SHORT_TYPE_GENRE = 0x0067656e; // Codes that have equivalent ID3 frames. - private static final int TYPE_COVER_ART = Util.getIntegerCodeForString("covr"); - private static final int TYPE_GENRE = Util.getIntegerCodeForString("gnre"); - private static final int TYPE_GROUPING = Util.getIntegerCodeForString("grp"); - private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); - private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); - private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); - private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); - private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); - private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); - private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); - private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); - private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); - private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); + private static final int TYPE_COVER_ART = 0x636f7672; + private static final int TYPE_GENRE = 0x676e7265; + private static final int TYPE_GROUPING = 0x00677270; + private static final int TYPE_DISK_NUMBER = 0x6469736b; + private static final int TYPE_TRACK_NUMBER = 0x74726b6e; + private static final int TYPE_TEMPO = 0x746d706f; + private static final int TYPE_COMPILATION = 0x6370696c; + private static final int TYPE_ALBUM_ARTIST = 0x61415254; + private static final int TYPE_SORT_TRACK_NAME = 0x736f6e6d; + private static final int TYPE_SORT_ALBUM = 0x736f616c; + private static final int TYPE_SORT_ARTIST = 0x736f6172; + private static final int TYPE_SORT_ALBUM_ARTIST = 0x736f6161; + private static final int TYPE_SORT_COMPOSER = 0x736f636f; // Types that do not have equivalent ID3 frames. - private static final int TYPE_RATING = Util.getIntegerCodeForString("rtng"); - private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); - private static final int TYPE_TV_SORT_SHOW = Util.getIntegerCodeForString("sosn"); - private static final int TYPE_TV_SHOW = Util.getIntegerCodeForString("tvsh"); + private static final int TYPE_RATING = 0x72746e67; + private static final int TYPE_GAPLESS_ALBUM = 0x70676170; + private static final int TYPE_TV_SORT_SHOW = 0x736f736e; + private static final int TYPE_TV_SHOW = 0x74767368; // Type for items that are intended for internal use by the player. - private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----"); + private static final int TYPE_INTERNAL = 0x2d2d2d2d; private static final int PICTURE_TYPE_FRONT_COVER = 3; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 75966bff66..16f5b1fb29 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -78,7 +77,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private static final int STATE_READING_SAMPLE = 2; /** Brand stored in the ftyp atom for QuickTime media. */ - private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt "); + private static final int BRAND_QUICKTIME = 0x71742020; /** * When seeking within the source, if the offset is greater than or equal to this value (or the diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java index 5c5afe39a8..95193785c0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.extractor.mp4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -32,32 +31,32 @@ import java.io.IOException; private static final int[] COMPATIBLE_BRANDS = new int[] { - Util.getIntegerCodeForString("isom"), - Util.getIntegerCodeForString("iso2"), - Util.getIntegerCodeForString("iso3"), - Util.getIntegerCodeForString("iso4"), - Util.getIntegerCodeForString("iso5"), - Util.getIntegerCodeForString("iso6"), - Util.getIntegerCodeForString("avc1"), - Util.getIntegerCodeForString("hvc1"), - Util.getIntegerCodeForString("hev1"), - Util.getIntegerCodeForString("av01"), - Util.getIntegerCodeForString("mp41"), - Util.getIntegerCodeForString("mp42"), - Util.getIntegerCodeForString("3g2a"), - Util.getIntegerCodeForString("3g2b"), - Util.getIntegerCodeForString("3gr6"), - Util.getIntegerCodeForString("3gs6"), - Util.getIntegerCodeForString("3ge6"), - Util.getIntegerCodeForString("3gg6"), - Util.getIntegerCodeForString("M4V "), - Util.getIntegerCodeForString("M4A "), - Util.getIntegerCodeForString("f4v "), - Util.getIntegerCodeForString("kddi"), - Util.getIntegerCodeForString("M4VP"), - Util.getIntegerCodeForString("qt "), // Apple QuickTime - Util.getIntegerCodeForString("MSNV"), // Sony PSP - Util.getIntegerCodeForString("dby1"), // Dolby Vision + 0x69736f6d, // isom + 0x69736f32, // iso2 + 0x69736f33, // iso3 + 0x69736f34, // iso4 + 0x69736f35, // iso5 + 0x69736f36, // iso6 + 0x61766331, // avc1 + 0x68766331, // hvc1 + 0x68657631, // hev1 + 0x61763031, // av01 + 0x6d703431, // mp41 + 0x6d703432, // mp42 + 0x33673261, // 3g2a + 0x33673262, // 3g2b + 0x33677236, // 3gr6 + 0x33677336, // 3gs6 + 0x33676536, // 3ge6 + 0x33676736, // 3gg6 + 0x4d345620, // M4V[space] + 0x4d344120, // M4A[space] + 0x66347620, // f4v[space] + 0x6b646469, // kddi + 0x4d345650, // M4VP + 0x71742020, // qt[space][space], Apple QuickTime + 0x4d534e56, // MSNV, Sony PSP + 0x64627931, // dby1, Dolby Vision }; /** @@ -188,7 +187,7 @@ import java.io.IOException; */ private static boolean isCompatibleBrand(int brand) { // Accept all brands starting '3gp'. - if (brand >>> 8 == Util.getIntegerCodeForString("3gp")) { + if (brand >>> 8 == 0x00336770) { return true; } for (int compatibleBrand : COMPATIBLE_BRANDS) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index 9d3635e8b3..7676926c4d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -123,6 +123,7 @@ public final class Track { * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no * such entry exists. */ + @Nullable public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) { return sampleDescriptionEncryptionBoxes == null ? null : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java index 5bd29c6e75..a35d211aa4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java @@ -52,7 +52,7 @@ public final class TrackEncryptionBox { * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the * track encryption box or sample group description box. Null otherwise. */ - public final byte[] defaultInitializationVector; + @Nullable public final byte[] defaultInitializationVector; /** * @param isEncrypted See {@link #isEncrypted}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index c83662ee83..51ab94ba0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -16,29 +16,32 @@ package com.google.android.exoplayer2.extractor.ogg; import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; /** Seeks in an Ogg stream. */ /* package */ final class DefaultOggSeeker implements OggSeeker { - @VisibleForTesting public static final int MATCH_RANGE = 72000; - @VisibleForTesting public static final int MATCH_BYTE_RANGE = 100000; + private static final int MATCH_RANGE = 72000; + private static final int MATCH_BYTE_RANGE = 100000; private static final int DEFAULT_OFFSET = 30000; private static final int STATE_SEEK_TO_END = 0; private static final int STATE_READ_LAST_PAGE = 1; private static final int STATE_SEEK = 2; - private static final int STATE_IDLE = 3; + private static final int STATE_SKIP = 3; + private static final int STATE_IDLE = 4; private final OggPageHeader pageHeader = new OggPageHeader(); - private final long startPosition; - private final long endPosition; + private final long payloadStartPosition; + private final long payloadEndPosition; private final StreamReader streamReader; private int state; @@ -54,26 +57,27 @@ import java.io.IOException; /** * Constructs an OggSeeker. * - * @param startPosition Start position of the payload (inclusive). - * @param endPosition End position of the payload (exclusive). * @param streamReader The {@link StreamReader} that owns this seeker. + * @param payloadStartPosition Start position of the payload (inclusive). + * @param payloadEndPosition End position of the payload (exclusive). * @param firstPayloadPageSize The total size of the first payload page, in bytes. * @param firstPayloadPageGranulePosition The granule position of the first payload page. - * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page in the - * ogg stream. + * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page. */ public DefaultOggSeeker( - long startPosition, - long endPosition, StreamReader streamReader, + long payloadStartPosition, + long payloadEndPosition, long firstPayloadPageSize, long firstPayloadPageGranulePosition, boolean firstPayloadPageIsLastPage) { - Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition); + Assertions.checkArgument( + payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition); this.streamReader = streamReader; - this.startPosition = startPosition; - this.endPosition = endPosition; - if (firstPayloadPageSize == endPosition - startPosition || firstPayloadPageIsLastPage) { + this.payloadStartPosition = payloadStartPosition; + this.payloadEndPosition = payloadEndPosition; + if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition + || firstPayloadPageIsLastPage) { totalGranules = firstPayloadPageGranulePosition; state = STATE_IDLE; } else { @@ -90,7 +94,7 @@ import java.io.IOException; positionBeforeSeekToEnd = input.getPosition(); state = STATE_READ_LAST_PAGE; // Seek to the end just before the last page of stream to get the duration. - long lastPageSearchPosition = endPosition - OggPageHeader.MAX_PAGE_SIZE; + long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE; if (lastPageSearchPosition > positionBeforeSeekToEnd) { return lastPageSearchPosition; } @@ -100,145 +104,110 @@ import java.io.IOException; state = STATE_IDLE; return positionBeforeSeekToEnd; case STATE_SEEK: - long currentGranule; - if (targetGranule == 0) { - currentGranule = 0; - } else { - long position = getNextSeekPosition(targetGranule, input); - if (position >= 0) { - return position; - } - currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2)); + long position = getNextSeekPosition(input); + if (position != C.POSITION_UNSET) { + return position; } + state = STATE_SKIP; + // Fall through. + case STATE_SKIP: + skipToPageOfTargetGranule(input); state = STATE_IDLE; - return -(currentGranule + 2); + return -(startGranule + 2); default: // Never happens. throw new IllegalStateException(); } } - @Override - public long startSeek(long timeUs) { - Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); - targetGranule = timeUs == 0 ? 0 : streamReader.convertTimeToGranule(timeUs); - state = STATE_SEEK; - resetSeeking(); - return targetGranule; - } - @Override public OggSeekMap createSeekMap() { return totalGranules != 0 ? new OggSeekMap() : null; } - @VisibleForTesting - public void resetSeeking() { - start = startPosition; - end = endPosition; + @Override + public void startSeek(long targetGranule) { + this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1); + state = STATE_SEEK; + start = payloadStartPosition; + end = payloadEndPosition; startGranule = 0; endGranule = totalGranules; } /** - * Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput} - * has to seek and then be passed for another call until a negative number is returned. If a - * negative number is returned the input is at a position which is before the target page and at - * which it is sensible to just skip pages to the target granule and pre-roll instead of doing - * another seek request. + * Performs a single step of a seeking binary search, returning the byte position from which data + * should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged. + * If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be + * called to skip to the target page. * - * @param targetGranule the target granule position to seek to. - * @param input the {@link ExtractorInput} to read from. - * @return the position to seek the {@link ExtractorInput} to for a next call or -(currentGranule - * + 2) if it's close enough to skip to the target page. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. + * @param input The {@link ExtractorInput} to read from. + * @return The byte position from which data should be provided for the next step, or {@link + * C#POSITION_UNSET} if the search has converged. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. */ - @VisibleForTesting - public long getNextSeekPosition(long targetGranule, ExtractorInput input) - throws IOException, InterruptedException { + private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException { if (start == end) { - return -(startGranule + 2); + return C.POSITION_UNSET; } - long initialPosition = input.getPosition(); + long currentPosition = input.getPosition(); if (!skipToNextPage(input, end)) { - if (start == initialPosition) { + if (start == currentPosition) { throw new IOException("No ogg page can be found."); } return start; } - pageHeader.populate(input, false); + pageHeader.populate(input, /* quiet= */ false); input.resetPeekPosition(); long granuleDistance = targetGranule - pageHeader.granulePosition; int pageSize = pageHeader.headerSize + pageHeader.bodySize; - if (granuleDistance < 0 || granuleDistance > MATCH_RANGE) { - if (granuleDistance < 0) { - end = initialPosition; - endGranule = pageHeader.granulePosition; - } else { - start = input.getPosition() + pageSize; - startGranule = pageHeader.granulePosition; - if (end - start + pageSize < MATCH_BYTE_RANGE) { - input.skipFully(pageSize); - return -(startGranule + 2); - } - } - - if (end - start < MATCH_BYTE_RANGE) { - end = start; - return start; - } - - long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); - long nextPosition = input.getPosition() - offset - + (granuleDistance * (end - start) / (endGranule - startGranule)); - - nextPosition = Math.max(nextPosition, start); - nextPosition = Math.min(nextPosition, end - 1); - return nextPosition; + if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) { + return C.POSITION_UNSET; } - // position accepted (before target granule and within MATCH_RANGE) - input.skipFully(pageSize); - return -(pageHeader.granulePosition + 2); + if (granuleDistance < 0) { + end = currentPosition; + endGranule = pageHeader.granulePosition; + } else { + start = input.getPosition() + pageSize; + startGranule = pageHeader.granulePosition; + } + + if (end - start < MATCH_BYTE_RANGE) { + end = start; + return start; + } + + long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); + long nextPosition = + input.getPosition() + - offset + + (granuleDistance * (end - start) / (endGranule - startGranule)); + return Util.constrainValue(nextPosition, start, end - 1); } - private long getEstimatedPosition(long position, long granuleDistance, long offset) { - position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset; - if (position < startPosition) { - position = startPosition; + /** + * Skips forward to the start of the page containing the {@code targetGranule}. + * + * @param input The {@link ExtractorInput} to read from. + * @throws ParserException If populating the page header fails. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + private void skipToPageOfTargetGranule(ExtractorInput input) + throws IOException, InterruptedException { + pageHeader.populate(input, /* quiet= */ false); + while (pageHeader.granulePosition <= targetGranule) { + input.skipFully(pageHeader.headerSize + pageHeader.bodySize); + start = input.getPosition(); + startGranule = pageHeader.granulePosition; + pageHeader.populate(input, /* quiet= */ false); } - if (position >= endPosition) { - position = endPosition - 1; - } - return position; - } - - private class OggSeekMap implements SeekMap { - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public SeekPoints getSeekPoints(long timeUs) { - if (timeUs == 0) { - return new SeekPoints(new SeekPoint(0, startPosition)); - } - long granule = streamReader.convertTimeToGranule(timeUs); - long estimatedPosition = getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET); - return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); - } - - @Override - public long getDurationUs() { - return streamReader.convertGranuleToTime(totalGranules); - } - + input.resetPeekPosition(); } /** @@ -251,7 +220,7 @@ import java.io.IOException; */ @VisibleForTesting void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException { - if (!skipToNextPage(input, endPosition)) { + if (!skipToNextPage(input, payloadEndPosition)) { // Not found until eof. throw new EOFException(); } @@ -263,13 +232,12 @@ import java.io.IOException; * @param input The {@code ExtractorInput} to skip to the next page. * @param limit The limit up to which the search should take place. * @return Whether the next page was found. - * @throws IOException thrown if peeking/reading from the input fails. - * @throws InterruptedException thrown if interrupted while peeking/reading from the input. + * @throws IOException If peeking/reading from the input fails. + * @throws InterruptedException If interrupted while peeking/reading from the input. */ - @VisibleForTesting - boolean skipToNextPage(ExtractorInput input, long limit) + private boolean skipToNextPage(ExtractorInput input, long limit) throws IOException, InterruptedException { - limit = Math.min(limit + 3, endPosition); + limit = Math.min(limit + 3, payloadEndPosition); byte[] buffer = new byte[2048]; int peekLength = buffer.length; while (true) { @@ -310,39 +278,35 @@ import java.io.IOException; long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException { skipToNextPage(input); pageHeader.reset(); - while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < endPosition) { - pageHeader.populate(input, false); + while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) { + pageHeader.populate(input, /* quiet= */ false); input.skipFully(pageHeader.headerSize + pageHeader.bodySize); } return pageHeader.granulePosition; } - /** - * Skips to the position of the start of the page containing the {@code targetGranule} and returns - * the granule of the page previous to the target page. - * - * @param input the {@link ExtractorInput} to read from. - * @param targetGranule the target granule. - * @param currentGranule the current granule or -1 if it's unknown. - * @return the granule of the prior page or the {@code currentGranule} if there isn't a prior - * page. - * @throws ParserException thrown if populating the page header fails. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. - */ - @VisibleForTesting - long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) - throws IOException, InterruptedException { - pageHeader.populate(input, false); - while (pageHeader.granulePosition < targetGranule) { - input.skipFully(pageHeader.headerSize + pageHeader.bodySize); - // Store in a member field to be able to resume after IOExceptions. - currentGranule = pageHeader.granulePosition; - // Peek next header. - pageHeader.populate(input, false); - } - input.resetPeekPosition(); - return currentGranule; - } + private final class OggSeekMap implements SeekMap { + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + long targetGranule = streamReader.convertTimeToGranule(timeUs); + long estimatedPosition = + payloadStartPosition + + (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules) + - DEFAULT_OFFSET; + estimatedPosition = + Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1); + return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); + } + + @Override + public long getDurationUs() { + return streamReader.convertGranuleToTime(totalGranules); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index 5eb0727908..4efd5c5e11 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -38,7 +38,7 @@ import java.util.List; private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; - private FlacStreamInfo streamInfo; + private FlacStreamMetadata streamMetadata; private FlacOggSeeker flacOggSeeker; public static boolean verifyBitstreamType(ParsableByteArray data) { @@ -50,7 +50,7 @@ import java.util.List; protected void reset(boolean headerData) { super.reset(headerData); if (headerData) { - streamInfo = null; + streamMetadata = null; flacOggSeeker = null; } } @@ -71,14 +71,24 @@ import java.util.List; protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) throws IOException, InterruptedException { byte[] data = packet.data; - if (streamInfo == null) { - streamInfo = new FlacStreamInfo(data, 17); + if (streamMetadata == null) { + streamMetadata = new FlacStreamMetadata(data, 17); byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks List initializationData = Collections.singletonList(metadata); - setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null, - Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate, - initializationData, null, 0, null); + setupData.format = + Format.createAudioSampleFormat( + null, + MimeTypes.AUDIO_FLAC, + null, + Format.NO_VALUE, + streamMetadata.bitRate(), + streamMetadata.channels, + streamMetadata.sampleRate, + initializationData, + null, + 0, + null); } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { flacOggSeeker = new FlacOggSeeker(); flacOggSeeker.parseSeekTable(packet); @@ -175,11 +185,9 @@ import java.util.List; } @Override - public long startSeek(long timeUs) { - long granule = convertTimeToGranule(timeUs); - int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); + public void startSeek(long targetGranule) { + int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true); pendingSeekGranule = seekPointGranules[index]; - return granule; } @Override @@ -211,7 +219,7 @@ import java.util.List; @Override public long getDurationUs() { - return streamInfo.durationUs(); + return streamMetadata.durationUs(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java index bbf7e2fc6b..c7fb3ff6a2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java @@ -19,7 +19,6 @@ 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.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; @@ -34,11 +33,17 @@ import java.io.IOException; public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT + MAX_PAGE_PAYLOAD; - private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS"); + private static final int TYPE_OGGS = 0x4f676753; public int revision; public int type; + /** + * The absolute granule position of the page. This is the total number of samples from the start + * of the file up to the end of the page. Samples partially in the page that continue on + * the next page do not count. + */ public long granulePosition; + public long streamSerialNumber; public long pageSequenceNumber; public long pageChecksum; @@ -72,10 +77,10 @@ import java.io.IOException; * Peeks an Ogg page header and updates this {@link OggPageHeader}. * * @param input The {@link ExtractorInput} to read from. - * @param quiet If {@code true}, no exceptions are thrown but {@code false} is returned if - * something goes wrong. - * @return {@code true} if the read was successful. The read fails if the end of the input is - * encountered without reading data. + * @param quiet Whether to return {@code false} rather than throwing an exception if the header + * cannot be populated. + * @return Whether the read was successful. The read fails if the end of the input is encountered + * without reading data. * @throws IOException If reading data fails or the stream is invalid. * @throws InterruptedException If the thread is interrupted. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java index aa88e5bf89..e4c3a163e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java @@ -33,16 +33,14 @@ import java.io.IOException; SeekMap createSeekMap(); /** - * Initializes a seek operation. + * Starts a seek operation. * - * @param timeUs The seek position in microseconds. - * @return The granule position targeted by the seek. + * @param targetGranule The target granule position. */ - long startSeek(long timeUs); + void startSeek(long targetGranule); /** - * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a - * progressive seek. + * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek. *

* If more data is required or if the position of the input needs to be modified then a position * from which data should be provided is returned. Else a negative value is returned. If a seek diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java index ff5f115573..90ae3f0f47 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java @@ -19,7 +19,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; @@ -38,7 +37,7 @@ import java.util.List; */ private static final int SAMPLE_RATE = 48000; - private static final int OPUS_CODE = Util.getIntegerCodeForString("Opus"); + private static final int OPUS_CODE = 0x4f707573; private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}; private boolean headerRead; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index e459ad1e58..d2671125e4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -91,7 +91,8 @@ import java.io.IOException; reset(!seekMapSet); } else { if (state != STATE_READ_HEADERS) { - targetGranule = oggSeeker.startSeek(timeUs); + targetGranule = convertTimeToGranule(timeUs); + oggSeeker.startSeek(targetGranule); state = STATE_READ_PAYLOAD; } } @@ -147,9 +148,9 @@ import java.io.IOException; boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream. oggSeeker = new DefaultOggSeeker( + this, payloadStartPosition, input.getLength(), - this, firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, firstPayloadPageHeader.granulePosition, isLastPage); @@ -248,13 +249,13 @@ import java.io.IOException; private static final class UnseekableOggSeeker implements OggSeeker { @Override - public long read(ExtractorInput input) throws IOException, InterruptedException { + public long read(ExtractorInput input) { return -1; } @Override - public long startSeek(long timeUs) { - return 0; + public void startSeek(long targetGranule) { + // Do nothing. } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index aa77aba30e..3d76276240 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -25,7 +25,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -35,7 +34,7 @@ public final class RawCcExtractor implements Extractor { private static final int SCRATCH_SIZE = 9; private static final int HEADER_SIZE = 8; - private static final int HEADER_ID = Util.getIntegerCodeForString("RCC\u0001"); + private static final int HEADER_ID = 0x52434301; private static final int TIMESTAMP_SIZE_V0 = 4; private static final int TIMESTAMP_SIZE_V1 = 8; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 889a49755a..b1d15b7189 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.audio.Ac3Util; @@ -27,7 +29,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -45,20 +46,14 @@ public final class Ac3Extractor implements Extractor { private static final int MAX_SNIFF_BYTES = 8 * 1024; private static final int AC3_SYNC_WORD = 0x0B77; private static final int MAX_SYNC_FRAME_SIZE = 2786; - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); - private final long firstSampleTimestampUs; private final Ac3Reader reader; private final ParsableByteArray sampleData; private boolean startedPacket; + /** Creates a new extractor for AC-3 bitstreams. */ public Ac3Extractor() { - this(0); - } - - public Ac3Extractor(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; reader = new Ac3Reader(); sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); } @@ -68,10 +63,10 @@ public final class Ac3Extractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { // Skip any ID3 headers. - ParsableByteArray scratch = new ParsableByteArray(10); + ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); int startPosition = 0; while (true) { - input.peekFully(scratch.data, 0, 10); + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); scratch.setPosition(0); if (scratch.readUnsignedInt24() != ID3_TAG) { break; @@ -142,7 +137,7 @@ public final class Ac3Extractor implements Extractor { if (!startedPacket) { // Pass data to the reader as though it's contained within a single infinitely long packet. - reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); + reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR); startedPacket = true; } // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java index 133c0f368b..205d71e16e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.audio.Ac4Util.AC40_SYNCWORD; import static com.google.android.exoplayer2.audio.Ac4Util.AC41_SYNCWORD; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.audio.Ac4Util; @@ -29,7 +31,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Extracts data from AC-4 bitstreams. */ @@ -53,9 +54,6 @@ public final class Ac4Extractor implements Extractor { /** The size of the frame header, in bytes. */ private static final int FRAME_HEADER_SIZE = 7; - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); - - private final long firstSampleTimestampUs; private final Ac4Reader reader; private final ParsableByteArray sampleData; @@ -63,12 +61,6 @@ public final class Ac4Extractor implements Extractor { /** Creates a new extractor for AC-4 bitstreams. */ public Ac4Extractor() { - this(/* firstSampleTimestampUs= */ 0); - } - - /** Creates a new extractor for AC-4 bitstreams, using the specified first sample timestamp. */ - public Ac4Extractor(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; reader = new Ac4Reader(); sampleData = new ParsableByteArray(READ_BUFFER_SIZE); } @@ -78,10 +70,10 @@ public final class Ac4Extractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { // Skip any ID3 headers. - ParsableByteArray scratch = new ParsableByteArray(10); + ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); int startPosition = 0; while (true) { - input.peekFully(scratch.data, /* offset= */ 0, /* length= */ 10); + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); scratch.setPosition(0); if (scratch.readUnsignedInt24() != ID3_TAG) { break; @@ -153,7 +145,7 @@ public final class Ac4Extractor implements Extractor { if (!startedPacket) { // Pass data to the reader as though it's contained within a single infinitely long packet. - reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); + reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR); startedPacket = true; } // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 9526a65766..381f19809b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -32,7 +34,6 @@ import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerat import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -66,7 +67,6 @@ public final class AdtsExtractor implements Extractor { public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; private static final int MAX_PACKET_SIZE = 2 * 1024; - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); /** * The maximum number of bytes to search when sniffing, excluding the header, before giving up. * Frame sizes are represented by 13-bit fields, so expect a valid frame in the first 8192 bytes. @@ -84,9 +84,8 @@ public final class AdtsExtractor implements Extractor { private final ParsableByteArray packetBuffer; private final ParsableByteArray scratch; private final ParsableBitArray scratchBits; - private final long firstStreamSampleTimestampUs; - private @Nullable ExtractorOutput extractorOutput; + @Nullable private ExtractorOutput extractorOutput; private long firstSampleTimestampUs; private long firstFramePosition; @@ -95,28 +94,24 @@ public final class AdtsExtractor implements Extractor { private boolean startedPacket; private boolean hasOutputSeekMap; + /** Creates a new extractor for ADTS bitstreams. */ public AdtsExtractor() { - this(0); - } - - public AdtsExtractor(long firstStreamSampleTimestampUs) { - this(/* firstStreamSampleTimestampUs= */ firstStreamSampleTimestampUs, /* flags= */ 0); + this(/* flags= */ 0); } /** - * @param firstStreamSampleTimestampUs The timestamp to be used for the first sample of the stream - * output from this extractor. + * Creates a new extractor for ADTS bitstreams. + * * @param flags Flags that control the extractor's behavior. */ - public AdtsExtractor(long firstStreamSampleTimestampUs, @Flags int flags) { - this.firstStreamSampleTimestampUs = firstStreamSampleTimestampUs; - this.firstSampleTimestampUs = firstStreamSampleTimestampUs; + public AdtsExtractor(@Flags int flags) { this.flags = flags; reader = new AdtsReader(true); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); averageFrameSize = C.LENGTH_UNSET; firstFramePosition = C.POSITION_UNSET; - scratch = new ParsableByteArray(10); + // Allocate scratch space for an ID3 header. The same buffer is also used to read 4 byte values. + scratch = new ParsableByteArray(ID3_HEADER_LENGTH); scratchBits = new ParsableBitArray(scratch.data); } @@ -173,7 +168,7 @@ public final class AdtsExtractor implements Extractor { public void seek(long position, long timeUs) { startedPacket = false; reader.seek(); - firstSampleTimestampUs = firstStreamSampleTimestampUs + timeUs; + firstSampleTimestampUs = timeUs; } @Override @@ -216,7 +211,7 @@ public final class AdtsExtractor implements Extractor { private int peekId3Header(ExtractorInput input) throws IOException, InterruptedException { int firstFramePosition = 0; while (true) { - input.peekFully(scratch.data, 0, 10); + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); scratch.setPosition(0); if (scratch.readUnsignedInt24() != ID3_TAG) { break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index 1564157d44..e7f2c1935b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -72,7 +72,7 @@ public final class H262Reader implements ElementaryStreamReader { this(null); } - public H262Reader(UserDataReader userDataReader) { + /* package */ H262Reader(UserDataReader userDataReader) { this.userDataReader = userDataReader; prefixFlags = new boolean[4]; csdBuffer = new CsdBuffer(128); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index f936fb9e43..77ec48d0a7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -33,8 +34,6 @@ public final class Id3Reader implements ElementaryStreamReader { private static final String TAG = "Id3Reader"; - private static final int ID3_HEADER_SIZE = 10; - private final ParsableByteArray id3Header; private TrackOutput output; @@ -48,7 +47,7 @@ public final class Id3Reader implements ElementaryStreamReader { private int sampleBytesRead; public Id3Reader() { - id3Header = new ParsableByteArray(ID3_HEADER_SIZE); + id3Header = new ParsableByteArray(ID3_HEADER_LENGTH); } @Override @@ -81,12 +80,12 @@ public final class Id3Reader implements ElementaryStreamReader { return; } int bytesAvailable = data.bytesLeft(); - if (sampleBytesRead < ID3_HEADER_SIZE) { + if (sampleBytesRead < ID3_HEADER_LENGTH) { // We're still reading the ID3 header. - int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_SIZE - sampleBytesRead); + int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_LENGTH - sampleBytesRead); System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead, headerBytesAvailable); - if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_SIZE) { + if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_LENGTH) { // We've finished reading the ID3 header. Extract the sample size. id3Header.setPosition(0); if ('I' != id3Header.readUnsignedByte() || 'D' != id3Header.readUnsignedByte() @@ -96,7 +95,7 @@ public final class Id3Reader implements ElementaryStreamReader { return; } id3Header.skipBytes(3); // version (2) + flags (1) - sampleSize = ID3_HEADER_SIZE + id3Header.readSynchSafeInt(); + sampleSize = ID3_HEADER_LENGTH + id3Header.readSynchSafeInt(); } } // Write data to the output. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index 895c224697..d032ef5883 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -26,10 +26,8 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; -/** - * Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. - */ -/* package */ final class SeiReader { +/** Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */ +public final class SeiReader { private final List closedCaptionFormats; private final TrackOutput[] outputs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index a2f8568cbb..d198e816d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -101,10 +101,10 @@ public final class TsExtractor implements Extractor { private static final int TS_PAT_PID = 0; private static final int MAX_PID_PLUS_ONE = 0x2000; - private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3"); - private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3"); - private static final long AC4_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-4"); - private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC"); + private static final long AC3_FORMAT_IDENTIFIER = 0x41432d33; + private static final long E_AC3_FORMAT_IDENTIFIER = 0x45414333; + private static final long AC4_FORMAT_IDENTIFIER = 0x41432d34; + private static final long HEVC_FORMAT_IDENTIFIER = 0x48455643; private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50; private static final int SNIFF_TS_PACKET_COUNT = 5; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 68d252e318..91097c9e5b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -87,12 +87,14 @@ public final class WavExtractor implements Extractor { if (!wavHeader.hasDataBounds()) { WavHeaderReader.skipToData(input, wavHeader); extractorOutput.seekMap(wavHeader); + } else if (input.getPosition() == 0) { + input.skipFully(wavHeader.getDataStartPosition()); } - long dataLimit = wavHeader.getDataLimit(); - Assertions.checkState(dataLimit != C.POSITION_UNSET); + long dataEndPosition = wavHeader.getDataEndPosition(); + Assertions.checkState(dataEndPosition != C.POSITION_UNSET); - long bytesLeft = dataLimit - input.getPosition(); + long bytesLeft = dataEndPosition - input.getPosition(); if (bytesLeft <= 0) { return Extractor.RESULT_END_OF_INPUT; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index c60117be60..6e3c5988a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -33,23 +33,29 @@ import com.google.android.exoplayer2.util.Util; private final int blockAlignment; /** Bits per sample for the audio data. */ private final int bitsPerSample; - /** The PCM encoding */ - @C.PcmEncoding - private final int encoding; + /** The PCM encoding. */ + @C.PcmEncoding private final int encoding; - /** Offset to the start of sample data. */ - private long dataStartPosition; - /** Total size in bytes of the sample data. */ - private long dataSize; + /** Position of the start of the sample data, in bytes. */ + private int dataStartPosition; + /** Position of the end of the sample data (exclusive), in bytes. */ + private long dataEndPosition; - public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment, - int bitsPerSample, @C.PcmEncoding int encoding) { + public WavHeader( + int numChannels, + int sampleRateHz, + int averageBytesPerSecond, + int blockAlignment, + int bitsPerSample, + @C.PcmEncoding int encoding) { this.numChannels = numChannels; this.sampleRateHz = sampleRateHz; this.averageBytesPerSecond = averageBytesPerSecond; this.blockAlignment = blockAlignment; this.bitsPerSample = bitsPerSample; this.encoding = encoding; + dataStartPosition = C.POSITION_UNSET; + dataEndPosition = C.POSITION_UNSET; } // Data bounds. @@ -57,22 +63,33 @@ import com.google.android.exoplayer2.util.Util; /** * Sets the data start position and size in bytes of sample data in this WAV. * - * @param dataStartPosition The data start position in bytes. - * @param dataSize The data size in bytes. + * @param dataStartPosition The position of the start of the sample data, in bytes. + * @param dataEndPosition The position of the end of the sample data (exclusive), in bytes. */ - public void setDataBounds(long dataStartPosition, long dataSize) { + public void setDataBounds(int dataStartPosition, long dataEndPosition) { this.dataStartPosition = dataStartPosition; - this.dataSize = dataSize; + this.dataEndPosition = dataEndPosition; } - /** Returns the data limit, or {@link C#POSITION_UNSET} if the data bounds have not been set. */ - public long getDataLimit() { - return hasDataBounds() ? (dataStartPosition + dataSize) : C.POSITION_UNSET; + /** + * Returns the position of the start of the sample data, in bytes, or {@link C#POSITION_UNSET} if + * the data bounds have not been set. + */ + public int getDataStartPosition() { + return dataStartPosition; + } + + /** + * Returns the position of the end of the sample data (exclusive), in bytes, or {@link + * C#POSITION_UNSET} if the data bounds have not been set. + */ + public long getDataEndPosition() { + return dataEndPosition; } /** Returns whether the data start position and size have been set. */ public boolean hasDataBounds() { - return dataStartPosition != 0 && dataSize != 0; + return dataStartPosition != C.POSITION_UNSET; } // SeekMap implementation. @@ -84,12 +101,13 @@ import com.google.android.exoplayer2.util.Util; @Override public long getDurationUs() { - long numFrames = dataSize / blockAlignment; + long numFrames = (dataEndPosition - dataStartPosition) / blockAlignment; return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; } @Override public SeekPoints getSeekPoints(long timeUs) { + long dataSize = dataEndPosition - dataStartPosition; long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; // Constrain to nearest preceding frame offset. positionOffset = (positionOffset / blockAlignment) * blockAlignment; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index c7b7a40ead..bbcb75aa2d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ @@ -92,8 +91,8 @@ import java.io.IOException; // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ... input.advancePeekPosition((int) chunkHeader.size - 16); - return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, - bitsPerSample, encoding); + return new WavHeader( + numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, bitsPerSample, encoding); } /** @@ -122,11 +121,13 @@ import java.io.IOException; ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); // Skip all chunks until we hit the data header. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - while (chunkHeader.id != Util.getIntegerCodeForString("data")) { - Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + while (chunkHeader.id != WavUtil.DATA_FOURCC) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) { + Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + } long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; // Override size of RIFF chunk, since it describes its size as the entire file. - if (chunkHeader.id == Util.getIntegerCodeForString("RIFF")) { + if (chunkHeader.id == WavUtil.RIFF_FOURCC) { bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; } if (bytesToSkip > Integer.MAX_VALUE) { @@ -138,7 +139,14 @@ import java.io.IOException; // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); - wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); + int dataStartPosition = (int) input.getPosition(); + long dataEndPosition = dataStartPosition + chunkHeader.size; + long inputLength = input.getLength(); + if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { + Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); + dataEndPosition = inputLength; + } + wavHeader.setDataBounds(dataStartPosition, dataEndPosition); } private WavHeaderReader() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index e79c776f88..d07def1894 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -53,6 +53,13 @@ public final class MediaCodecInfo { /** The MIME type handled by the codec, or {@code null} if this is a passthrough codec. */ @Nullable public final String mimeType; + /** + * The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this + * is a passthrough codec. Equal to {@link #mimeType} unless the codec is known to use a + * non-standard MIME type alias. + */ + @Nullable public final String codecMimeType; + /** * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not * known. @@ -98,6 +105,7 @@ public final class MediaCodecInfo { return new MediaCodecInfo( name, /* mimeType= */ null, + /* codecMimeType= */ null, /* capabilities= */ null, /* passthrough= */ true, /* forceDisableAdaptive= */ false, @@ -109,26 +117,8 @@ public final class MediaCodecInfo { * * @param name The name of the {@link MediaCodec}. * @param mimeType A mime type supported by the {@link MediaCodec}. - * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or - * {@code null} if not known. - * @return The created instance. - */ - public static MediaCodecInfo newInstance( - String name, String mimeType, @Nullable CodecCapabilities capabilities) { - return new MediaCodecInfo( - name, - mimeType, - capabilities, - /* passthrough= */ false, - /* forceDisableAdaptive= */ false, - /* forceSecure= */ false); - } - - /** - * Creates an instance. - * - * @param name The name of the {@link MediaCodec}. - * @param mimeType A mime type supported by the {@link MediaCodec}. + * @param codecMimeType The MIME type that the codec uses for media of type {@code #mimeType}. + * Equal to {@code mimeType} unless the codec is known to use a non-standard MIME type alias. * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or * {@code null} if not known. * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. @@ -138,22 +128,31 @@ public final class MediaCodecInfo { public static MediaCodecInfo newInstance( String name, String mimeType, + String codecMimeType, @Nullable CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) { return new MediaCodecInfo( - name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure); + name, + mimeType, + codecMimeType, + capabilities, + /* passthrough= */ false, + forceDisableAdaptive, + forceSecure); } private MediaCodecInfo( String name, @Nullable String mimeType, + @Nullable String codecMimeType, @Nullable CodecCapabilities capabilities, boolean passthrough, boolean forceDisableAdaptive, boolean forceSecure) { this.name = Assertions.checkNotNull(name); this.mimeType = mimeType; + this.codecMimeType = codecMimeType; this.capabilities = capabilities; this.passthrough = passthrough; adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); @@ -198,7 +197,7 @@ public final class MediaCodecInfo { * @throws MediaCodecUtil.DecoderQueryException Thrown if an error occurs while querying decoders. */ public boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQueryException { - if (!isCodecSupported(format.codecs)) { + if (!isCodecSupported(format)) { return false; } @@ -226,25 +225,25 @@ public final class MediaCodecInfo { } /** - * Whether the decoder supports the given {@code codec}. If there is insufficient information to - * decide, returns true. + * Whether the decoder supports the codec of the given {@code format}. If there is insufficient + * information to decide, returns true. * - * @param codec Codec string as defined in RFC 6381. - * @return True if the given codec is supported by the decoder. + * @param format The input media format. + * @return True if the codec of the given {@code format} is supported by the decoder. */ - public boolean isCodecSupported(String codec) { - if (codec == null || mimeType == null) { + public boolean isCodecSupported(Format format) { + if (format.codecs == null || mimeType == null) { return true; } - String codecMimeType = MimeTypes.getMediaMimeType(codec); + String codecMimeType = MimeTypes.getMediaMimeType(format.codecs); if (codecMimeType == null) { return true; } if (!mimeType.equals(codecMimeType)) { - logNoSupport("codec.mime " + codec + ", " + codecMimeType); + logNoSupport("codec.mime " + format.codecs + ", " + codecMimeType); return false; } - Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codec); + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel == null) { // If we don't know any better, we assume that the profile and level are supported. return true; @@ -261,7 +260,7 @@ public final class MediaCodecInfo { return true; } } - logNoSupport("codec.profileLevel, " + codec + ", " + codecMimeType); + logNoSupport("codec.profileLevel, " + format.codecs + ", " + codecMimeType); return false; } @@ -279,8 +278,7 @@ public final class MediaCodecInfo { if (isVideo) { return adaptive; } else { - Pair codecProfileLevel = - MediaCodecUtil.getCodecProfileAndLevel(format.codecs); + Pair codecProfileLevel = MediaCodecUtil.getCodecProfileAndLevel(format); return codecProfileLevel != null && codecProfileLevel.first == CodecProfileLevel.AACObjectXHE; } } @@ -314,9 +312,9 @@ public final class MediaCodecInfo { } // Check the codec profile levels support adaptation. Pair oldCodecProfileLevel = - MediaCodecUtil.getCodecProfileAndLevel(oldFormat.codecs); + MediaCodecUtil.getCodecProfileAndLevel(oldFormat); Pair newCodecProfileLevel = - MediaCodecUtil.getCodecProfileAndLevel(newFormat.codecs); + MediaCodecUtil.getCodecProfileAndLevel(newFormat); if (oldCodecProfileLevel == null || newCodecProfileLevel == null) { return false; } @@ -520,9 +518,15 @@ public final class MediaCodecInfo { @TargetApi(21) private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width, int height, double frameRate) { - return frameRate == Format.NO_VALUE || frameRate <= 0 - ? capabilities.isSizeSupported(width, height) - : capabilities.areSizeAndRateSupported(width, height, frameRate); + if (frameRate == Format.NO_VALUE || frameRate <= 0) { + return capabilities.isSizeSupported(width, height); + } else { + // The signaled frame rate may be slightly higher than the actual frame rate, so we take the + // floor to avoid situations where a range check in areSizeAndRateSupported fails due to + // slightly exceeding the limits for a standard format (e.g., 1080p at 30 fps). + double floorFrameRate = Math.floor(frameRate); + return capabilities.areSizeAndRateSupported(width, height, floorFrameRate); + } } @TargetApi(23) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 06b76781b4..974e033b67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -53,7 +53,6 @@ import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -81,21 +80,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer { public final boolean secureDecoderRequired; /** - * The name of the decoder that failed to initialize. Null if no suitable decoder was found. + * The {@link MediaCodecInfo} of the decoder that failed to initialize. Null if no suitable + * decoder was found. */ - public final String decoderName; + @Nullable public final MediaCodecInfo codecInfo; - /** - * An optional developer-readable diagnostic information string. May be null. - */ - public final String diagnosticInfo; + /** An optional developer-readable diagnostic information string. May be null. */ + @Nullable public final String diagnosticInfo; /** * If the decoder failed to initialize and another decoder being used as a fallback also failed * to initialize, the {@link DecoderInitializationException} for the fallback decoder. Null if * there was no fallback decoder or no suitable decoders were found. */ - public final @Nullable DecoderInitializationException fallbackDecoderInitializationException; + @Nullable public final DecoderInitializationException fallbackDecoderInitializationException; public DecoderInitializationException(Format format, Throwable cause, boolean secureDecoderRequired, int errorCode) { @@ -104,19 +102,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { cause, format.sampleMimeType, secureDecoderRequired, - /* decoderName= */ null, + /* mediaCodecInfo= */ null, buildCustomDiagnosticInfo(errorCode), /* fallbackDecoderInitializationException= */ null); } - public DecoderInitializationException(Format format, Throwable cause, - boolean secureDecoderRequired, String decoderName) { + public DecoderInitializationException( + Format format, + Throwable cause, + boolean secureDecoderRequired, + MediaCodecInfo mediaCodecInfo) { this( - "Decoder init failed: " + decoderName + ", " + format, + "Decoder init failed: " + mediaCodecInfo.name + ", " + format, cause, format.sampleMimeType, secureDecoderRequired, - decoderName, + mediaCodecInfo, Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null, /* fallbackDecoderInitializationException= */ null); } @@ -126,13 +127,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { Throwable cause, String mimeType, boolean secureDecoderRequired, - @Nullable String decoderName, + @Nullable MediaCodecInfo mediaCodecInfo, @Nullable String diagnosticInfo, @Nullable DecoderInitializationException fallbackDecoderInitializationException) { super(message, cause); this.mimeType = mimeType; this.secureDecoderRequired = secureDecoderRequired; - this.decoderName = decoderName; + this.codecInfo = mediaCodecInfo; this.diagnosticInfo = diagnosticInfo; this.fallbackDecoderInitializationException = fallbackDecoderInitializationException; } @@ -145,7 +146,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { getCause(), mimeType, secureDecoderRequired, - decoderName, + codecInfo, diagnosticInfo, fallbackException); } @@ -160,9 +161,34 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private static String buildCustomDiagnosticInfo(int errorCode) { String sign = errorCode < 0 ? "neg_" : ""; - return "com.google.android.exoplayer.MediaCodecTrackRenderer_" + sign + Math.abs(errorCode); + return "com.google.android.exoplayer2.mediacodec.MediaCodecRenderer_" + + sign + + Math.abs(errorCode); + } + } + + /** Thrown when a failure occurs in the decoder. */ + public static class DecoderException extends Exception { + + /** The {@link MediaCodecInfo} of the decoder that failed. Null if unknown. */ + @Nullable public final MediaCodecInfo codecInfo; + + /** An optional developer-readable diagnostic information string. May be null. */ + @Nullable public final String diagnosticInfo; + + public DecoderException(Throwable cause, @Nullable MediaCodecInfo codecInfo) { + super("Decoder failed: " + (codecInfo == null ? null : codecInfo.name), cause); + this.codecInfo = codecInfo; + diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null; } + @TargetApi(21) + private static String getDiagnosticInfoV21(Throwable cause) { + if (cause instanceof CodecException) { + return ((CodecException) cause).getDiagnosticInfo(); + } + return null; + } } /** Indicates no codec operating rate should be set. */ @@ -329,14 +355,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private int inputIndex; private int outputIndex; private ByteBuffer outputBuffer; - private boolean shouldSkipOutputBuffer; + private boolean isDecodeOnlyOutputBuffer; + private boolean isLastOutputBuffer; private boolean codecReconfigured; @ReconfigurationState private int codecReconfigurationState; @DrainState private int codecDrainState; @DrainAction private int codecDrainAction; private boolean codecReceivedBuffers; private boolean codecReceivedEos; - + private long lastBufferInStreamPresentationTimeUs; + private long largestQueuedPresentationTimeUs; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; @@ -454,15 +482,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * no codec operating rate should be set. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ protected abstract void configureCodec( MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto, - float codecOperatingRate) - throws DecoderQueryException; + float codecOperatingRate); protected final void maybeInitCodec() throws ExoPlaybackException { if (codec != null || inputFormat == null) { @@ -601,6 +627,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { waitingForKeys = false; codecHotswapDeadlineMs = C.TIME_UNSET; decodeOnlyPresentationTimestamps.clear(); + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; try { if (codec != null) { decoderCounters.decoderReleaseCount++; @@ -636,31 +664,40 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (outputStreamEnded) { - renderToEndOfStream(); - return; - } - if (inputFormat == null && !readToFlagsOnlyBuffer(/* requireFormat= */ true)) { + try { + if (outputStreamEnded) { + renderToEndOfStream(); + return; + } + if (inputFormat == null && !readToFlagsOnlyBuffer(/* requireFormat= */ true)) { // We still don't have a format and can't make progress without one. return; + } + // We have a format. + maybeInitCodec(); + if (codec != null) { + long drainStartTimeMs = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} + while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {} + TraceUtil.endSection(); + } else { + decoderCounters.skippedInputBufferCount += skipSource(positionUs); + // We need to read any format changes despite not having a codec so that drmSession can be + // updated, and so that we have the most recent format should the codec be initialized. We + // may + // also reach the end of the stream. Note that readSource will not read a sample into a + // flags-only buffer. + readToFlagsOnlyBuffer(/* requireFormat= */ false); + } + decoderCounters.ensureUpdated(); + } catch (IllegalStateException e) { + if (isMediaCodecException(e)) { + throw ExoPlaybackException.createForRenderer( + createDecoderException(e, getCodecInfo()), getIndex()); + } + throw e; } - // We have a format. - maybeInitCodec(); - if (codec != null) { - long drainStartTimeMs = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} - while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {} - TraceUtil.endSection(); - } else { - decoderCounters.skippedInputBufferCount += skipSource(positionUs); - // We need to read any format changes despite not having a codec so that drmSession can be - // updated, and so that we have the most recent format should the codec be initialized. We may - // also reach the end of the stream. Note that readSource will not read a sample into a - // flags-only buffer. - readToFlagsOnlyBuffer(/* requireFormat= */ false); - } - decoderCounters.ensureUpdated(); } /** @@ -707,10 +744,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { waitingForFirstSyncSample = true; codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; - shouldSkipOutputBuffer = false; + isDecodeOnlyOutputBuffer = false; + isLastOutputBuffer = false; waitingForKeys = false; decodeOnlyPresentationTimestamps.clear(); + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; codecDrainState = DRAIN_STATE_NONE; codecDrainAction = DRAIN_ACTION_NONE; // Reconfiguration data sent shortly before the flush may not have been processed by the @@ -721,6 +761,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } + protected DecoderException createDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo) { + return new DecoderException(cause, codecInfo); + } + /** Reads into {@link #flagsOnlyBuffer} and returns whether a format was read. */ private boolean readToFlagsOnlyBuffer(boolean requireFormat) throws ExoPlaybackException { flagsOnlyBuffer.clear(); @@ -742,11 +787,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { List allAvailableCodecInfos = getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder); + availableCodecInfos = new ArrayDeque<>(); if (enableDecoderFallback) { - availableCodecInfos = new ArrayDeque<>(allAvailableCodecInfos); - } else { - availableCodecInfos = - new ArrayDeque<>(Collections.singletonList(allAvailableCodecInfos.get(0))); + availableCodecInfos.addAll(allAvailableCodecInfos); + } else if (!allAvailableCodecInfos.isEmpty()) { + availableCodecInfos.add(allAvailableCodecInfos.get(0)); } preferredDecoderInitializationException = null; } catch (DecoderQueryException e) { @@ -781,7 +826,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { availableCodecInfos.removeFirst(); DecoderInitializationException exception = new DecoderInitializationException( - inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo.name); + inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo); if (preferredDecoderInitializationException == null) { preferredDecoderInitializationException = exception; } else { @@ -884,7 +929,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecDrainAction = DRAIN_ACTION_NONE; codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; - shouldSkipOutputBuffer = false; + isDecodeOnlyOutputBuffer = false; + isLastOutputBuffer = false; waitingForFirstSyncSample = true; decoderCounters.decoderInitCount++; @@ -942,21 +988,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private void setSourceDrmSession(@Nullable DrmSession session) { - DrmSession previous = sourceDrmSession; + DrmSession.replaceSessionReferences(sourceDrmSession, session); sourceDrmSession = session; - releaseDrmSessionIfUnused(previous); } private void setCodecDrmSession(@Nullable DrmSession session) { - DrmSession previous = codecDrmSession; + DrmSession.replaceSessionReferences(codecDrmSession, session); codecDrmSession = session; - releaseDrmSessionIfUnused(previous); - } - - private void releaseDrmSessionIfUnused(@Nullable DrmSession session) { - if (session != null && session != sourceDrmSession && session != codecDrmSession) { - drmSessionManager.releaseSession(session); - } } /** @@ -1019,6 +1057,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { result = readSource(formatHolder, buffer, false); } + if (hasReadStreamToEnd()) { + // Notify output queue of the last buffer's timestamp. + lastBufferInStreamPresentationTimeUs = largestQueuedPresentationTimeUs; + } + if (result == C.RESULT_NOTHING_READ) { return false; } @@ -1091,6 +1134,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { formatQueue.add(presentationTimeUs, inputFormat); waitingForFirstSampleInFormat = false; } + largestQueuedPresentationTimeUs = + Math.max(largestQueuedPresentationTimeUs, presentationTimeUs); buffer.flip(); onQueueInputBuffer(buffer); @@ -1144,6 +1189,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. * @throws ExoPlaybackException If an error occurs re-initializing the {@link MediaCodec}. */ + @SuppressWarnings("unchecked") protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { Format oldFormat = inputFormat; Format newFormat = formatHolder.format; @@ -1154,18 +1200,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer { !Util.areEqual(newFormat.drmInitData, oldFormat == null ? null : oldFormat.drmInitData); if (drmInitDataChanged) { if (newFormat.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(), newFormat.drmInitData); + if (sourceDrmSession != null) { + sourceDrmSession.releaseReference(); + } + sourceDrmSession = session; } - DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (session == sourceDrmSession || session == codecDrmSession) { - // We already had this session. The manager must be reference counting, so release it once - // to get the count attributed to this renderer back down to 1. - drmSessionManager.releaseSession(session); - } - setSourceDrmSession(session); } else { setSourceDrmSession(null); } @@ -1462,7 +1510,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputBuffer.position(outputBufferInfo.offset); outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); } - shouldSkipOutputBuffer = shouldSkipOutputBuffer(outputBufferInfo.presentationTimeUs); + isDecodeOnlyOutputBuffer = isDecodeOnlyBuffer(outputBufferInfo.presentationTimeUs); + isLastOutputBuffer = + lastBufferInStreamPresentationTimeUs == outputBufferInfo.presentationTimeUs; updateOutputFormatForTime(outputBufferInfo.presentationTimeUs); } @@ -1478,7 +1528,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, outputFormat); } catch (IllegalStateException e) { processEndOfStream(); @@ -1498,7 +1549,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, outputFormat); } @@ -1565,7 +1617,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param bufferIndex The index of the output buffer. * @param bufferFlags The flags attached to the output buffer. * @param bufferPresentationTimeUs The presentation time of the output buffer in microseconds. - * @param shouldSkip Whether the buffer should be skipped (i.e. not rendered). + * @param isDecodeOnlyBuffer Whether the buffer was marked with {@link C#BUFFER_FLAG_DECODE_ONLY} + * by the source. + * @param isLastBuffer Whether the buffer is the last sample of the current stream. * @param format The format associated with the buffer. * @return Whether the output buffer was fully processed (e.g. rendered or skipped). * @throws ExoPlaybackException If an error occurs processing the output buffer. @@ -1578,7 +1632,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException; @@ -1658,7 +1713,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecDrainAction = DRAIN_ACTION_NONE; } - private boolean shouldSkipOutputBuffer(long presentationTimeUs) { + private boolean isDecodeOnlyBuffer(long presentationTimeUs) { // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would // box presentationTimeUs, creating a Long object that would need to be garbage collected. int size = decodeOnlyPresentationTimestamps.size(); @@ -1687,6 +1742,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return cryptoInfo; } + private static boolean isMediaCodecException(IllegalStateException error) { + if (Util.SDK_INT >= 21 && isMediaCodecExceptionV21(error)) { + return true; + } + StackTraceElement[] stackTrace = error.getStackTrace(); + return stackTrace.length > 0 && stackTrace[0].getClassName().equals("android.media.MediaCodec"); + } + + @TargetApi(21) + private static boolean isMediaCodecExceptionV21(IllegalStateException error) { + return error instanceof MediaCodec.CodecException; + } + /** * Returns whether the device needs keys to have been loaded into the {@link DrmSession} before * codec configuration. @@ -1788,9 +1856,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) { String name = codecInfo.name; - return (Util.SDK_INT <= 17 - && ("OMX.rk.video_decoder.avc".equals(name) - || "OMX.allwinner.video.decoder.avc".equals(name))) + return (Util.SDK_INT <= 25 && "OMX.rk.video_decoder.avc".equals(name)) + || (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name)) || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java index 41cb4ee04a..a639cf9a1b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java @@ -40,7 +40,8 @@ public interface MediaCodecSelector { } @Override - public @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { + @Nullable + public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { return MediaCodecUtil.getPassthroughDecoderInfo(); } }; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index f3936e5dc2..cd4c4863ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -25,10 +25,12 @@ import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; import android.util.SparseIntArray; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.ColorInfo; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -81,10 +83,13 @@ public final class MediaCodecUtil { private static final Map DOLBY_VISION_STRING_TO_LEVEL; private static final String CODEC_ID_DVHE = "dvhe"; private static final String CODEC_ID_DVH1 = "dvh1"; + // AV1. + private static final SparseIntArray AV1_LEVEL_NUMBER_TO_CONST; + private static final String CODEC_ID_AV01 = "av01"; // MP4A AAC. private static final SparseIntArray MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE; private static final String CODEC_ID_MP4A = "mp4a"; - + // Lazily initialized. private static int maxH264DecodableFrameSize = -1; @@ -166,12 +171,12 @@ public final class MediaCodecUtil { Util.SDK_INT >= 21 ? new MediaCodecListCompatV21(secure, tunneling) : new MediaCodecListCompatV16(); - ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); + ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList); if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. mediaCodecList = new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); + decoderInfos = getDecoderInfosInternal(key, mediaCodecList); if (!decoderInfos.isEmpty()) { Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + ". Assuming: " + decoderInfos.get(0).name); @@ -227,35 +232,34 @@ public final class MediaCodecUtil { } /** - * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given - * codec description string (as defined by RFC 6381). + * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the codec + * description string (as defined by RFC 6381) of the given format. * - * @param codec A codec description string, as defined by RFC 6381, or {@code null} if not known. - * @return A pair (profile constant, level constant) if {@code codec} is well-formed and - * recognized, or null otherwise + * @param format Media format with a codec description string, as defined by RFC 6381. + * @return A pair (profile constant, level constant) if the codec of the {@code format} is + * well-formed and recognized, or null otherwise. */ - @Nullable - public static Pair getCodecProfileAndLevel(@Nullable String codec) { - if (codec == null) { + public static Pair getCodecProfileAndLevel(Format format) { + if (format.codecs == null) { return null; } - // TODO: Check codec profile/level for AV1 once targeting Android Q and [Internal: b/128552878] - // has been fixed. - String[] parts = codec.split("\\."); + String[] parts = format.codecs.split("\\."); switch (parts[0]) { case CODEC_ID_AVC1: case CODEC_ID_AVC2: - return getAvcProfileAndLevel(codec, parts); + return getAvcProfileAndLevel(format.codecs, parts); case CODEC_ID_VP09: - return getVp9ProfileAndLevel(codec, parts); + return getVp9ProfileAndLevel(format.codecs, parts); case CODEC_ID_HEV1: case CODEC_ID_HVC1: - return getHevcProfileAndLevel(codec, parts); + return getHevcProfileAndLevel(format.codecs, parts); case CODEC_ID_DVHE: case CODEC_ID_DVH1: - return getDolbyVisionProfileAndLevel(codec, parts); + return getDolbyVisionProfileAndLevel(format.codecs, parts); + case CODEC_ID_AV01: + return getAv1ProfileAndLevel(format.codecs, parts, format.colorInfo); case CODEC_ID_MP4A: - return getAacCodecProfileAndLevel(codec, parts); + return getAacCodecProfileAndLevel(format.codecs, parts); default: return null; } @@ -264,18 +268,16 @@ public final class MediaCodecUtil { // Internal methods. /** - * Returns {@link MediaCodecInfo}s for the given codec {@code key} in the order given by + * Returns {@link MediaCodecInfo}s for the given codec {@link CodecKey} in the order given by * {@code mediaCodecList}. * * @param key The codec key. * @param mediaCodecList The codec list. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. * @return The codec information for usable codecs matching the specified key. * @throws DecoderQueryException If there was an error querying the available decoders. */ - private static ArrayList getDecoderInfosInternal(CodecKey key, - MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException { + private static ArrayList getDecoderInfosInternal( + CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { try { ArrayList decoderInfos = new ArrayList<>(); String mimeType = key.mimeType; @@ -285,28 +287,27 @@ public final class MediaCodecUtil { for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); String name = codecInfo.getName(); - String supportedType = - getCodecSupportedType(codecInfo, name, secureDecodersExplicit, requestedMimeType); - if (supportedType == null) { + String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType); + if (codecMimeType == null) { continue; } try { - CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType); + CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(codecMimeType); boolean tunnelingSupported = mediaCodecList.isFeatureSupported( - CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); boolean tunnelingRequired = mediaCodecList.isFeatureRequired( - CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); if ((!key.tunneling && tunnelingRequired) || (key.tunneling && !tunnelingSupported)) { continue; } boolean secureSupported = mediaCodecList.isFeatureSupported( - CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); boolean secureRequired = mediaCodecList.isFeatureRequired( - CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) { continue; } @@ -315,12 +316,18 @@ public final class MediaCodecUtil { || (!secureDecodersExplicit && !key.secure)) { decoderInfos.add( MediaCodecInfo.newInstance( - name, mimeType, capabilities, forceDisableAdaptive, /* forceSecure= */ false)); + name, + mimeType, + codecMimeType, + capabilities, + forceDisableAdaptive, + /* forceSecure= */ false)); } else if (!secureDecodersExplicit && secureSupported) { decoderInfos.add( MediaCodecInfo.newInstance( name + ".secure", mimeType, + codecMimeType, capabilities, forceDisableAdaptive, /* forceSecure= */ true)); @@ -334,7 +341,7 @@ public final class MediaCodecUtil { } else { // Rethrow error querying primary codec capabilities, or secondary codec // capabilities if API level is greater than 23. - Log.e(TAG, "Failed to query codec " + name + " (" + supportedType + ")"); + Log.e(TAG, "Failed to query codec " + name + " (" + codecMimeType + ")"); throw e; } } @@ -348,42 +355,49 @@ public final class MediaCodecUtil { } /** - * Returns the codec's supported type for decoding {@code requestedMimeType} on the current - * device, or {@code null} if the codec can't be used. + * Returns the codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. * * @param info The codec information. * @param name The name of the codec * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. - * @return The codec's supported type for decoding {@code requestedMimeType}, or {@code null} if - * the codec can't be used. + * @param mimeType The MIME type. + * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. If non-null, the returned type will be equal to {@code mimeType} + * except in cases where the codec is known to use a non-standard MIME type alias. */ @Nullable - private static String getCodecSupportedType( + private static String getCodecMimeType( android.media.MediaCodecInfo info, String name, boolean secureDecodersExplicit, - String requestedMimeType) { - if (isCodecUsableDecoder(info, name, secureDecodersExplicit, requestedMimeType)) { - if (requestedMimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { - // Handle decoders that declare support for DV via MIME types that aren't - // video/dolby-vision. - if ("OMX.MS.HEVCDV.Decoder".equals(name)) { - return "video/hevcdv"; - } else if ("OMX.RTK.video.decoder".equals(name) - || "OMX.realtek.video.decoder.tunneled".equals(name)) { - return "video/dv_hevc"; - } - } + String mimeType) { + if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) { + return null; + } - String[] supportedTypes = info.getSupportedTypes(); - for (String supportedType : supportedTypes) { - if (supportedType.equalsIgnoreCase(requestedMimeType)) { - return supportedType; - } + String[] supportedTypes = info.getSupportedTypes(); + for (String supportedType : supportedTypes) { + if (supportedType.equalsIgnoreCase(mimeType)) { + return supportedType; } } + + if (mimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { + // Handle decoders that declare support for DV via MIME types that aren't + // video/dolby-vision. + if ("OMX.MS.HEVCDV.Decoder".equals(name)) { + return "video/hevcdv"; + } else if ("OMX.RTK.video.decoder".equals(name) + || "OMX.realtek.video.decoder.tunneled".equals(name)) { + return "video/dv_hevc"; + } + } else if (mimeType.equals(MimeTypes.AUDIO_ALAC) && "OMX.lge.alac.decoder".equals(name)) { + return "audio/x-lg-alac"; + } else if (mimeType.equals(MimeTypes.AUDIO_FLAC) && "OMX.lge.flac.decoder".equals(name)) { + return "audio/x-lg-flac"; + } + return null; } @@ -393,12 +407,14 @@ public final class MediaCodecUtil { * @param info The codec information. * @param name The name of the codec * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. + * @param mimeType The MIME type. * @return Whether the specified codec is usable for decoding on the current device. */ - private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit, String requestedMimeType) { + private static boolean isCodecUsableDecoder( + android.media.MediaCodecInfo info, + String name, + boolean secureDecodersExplicit, + String mimeType) { if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { return false; } @@ -487,8 +503,7 @@ public final class MediaCodecUtil { } // MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041]. - if (MimeTypes.AUDIO_E_AC3_JOC.equals(requestedMimeType) - && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType) && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { return false; } @@ -512,7 +527,12 @@ public final class MediaCodecUtil { // name. See Issue #5782. decoderInfos.add( MediaCodecInfo.newInstance( - "OMX.google.raw.decoder", MimeTypes.AUDIO_RAW, /* capabilities= */ null)); + /* name= */ "OMX.google.raw.decoder", + /* mimeType= */ MimeTypes.AUDIO_RAW, + /* codecMimeType= */ MimeTypes.AUDIO_RAW, + /* capabilities= */ null, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); } // Work around inconsistent raw audio decoding behavior across different devices. sortByScore( @@ -684,6 +704,52 @@ public final class MediaCodecUtil { return new Pair<>(profile, level); } + private static Pair getAv1ProfileAndLevel( + String codec, String[] parts, @Nullable ColorInfo colorInfo) { + if (parts.length < 4) { + Log.w(TAG, "Ignoring malformed AV1 codec string: " + codec); + return null; + } + int profileInteger; + int levelInteger; + int bitDepthInteger; + try { + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2].substring(0, 2)); + bitDepthInteger = Integer.parseInt(parts[3]); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed AV1 codec string: " + codec); + return null; + } + + if (profileInteger != 0) { + Log.w(TAG, "Unknown AV1 profile: " + profileInteger); + return null; + } + if (bitDepthInteger != 8 && bitDepthInteger != 10) { + Log.w(TAG, "Unknown AV1 bit depth: " + bitDepthInteger); + return null; + } + int profile; + if (bitDepthInteger == 8) { + profile = CodecProfileLevel.AV1ProfileMain8; + } else if (colorInfo != null + && (colorInfo.hdrStaticInfo != null + || colorInfo.colorTransfer == C.COLOR_TRANSFER_HLG + || colorInfo.colorTransfer == C.COLOR_TRANSFER_ST2084)) { + profile = CodecProfileLevel.AV1ProfileMain10HDR10; + } else { + profile = CodecProfileLevel.AV1ProfileMain10; + } + + int level = AV1_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { + Log.w(TAG, "Unknown AV1 level: " + levelInteger); + return null; + } + return new Pair<>(profile, level); + } + /** * Conversion values taken from ISO 14496-10 Table A-1. * @@ -1010,6 +1076,34 @@ public final class MediaCodecUtil { DOLBY_VISION_STRING_TO_LEVEL.put("08", CodecProfileLevel.DolbyVisionLevelUhd48); DOLBY_VISION_STRING_TO_LEVEL.put("09", CodecProfileLevel.DolbyVisionLevelUhd60); + // See https://aomediacodec.github.io/av1-spec/av1-spec.pdf Annex A: Profiles and levels for + // more information on mapping AV1 codec strings to levels. + AV1_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); + AV1_LEVEL_NUMBER_TO_CONST.put(0, CodecProfileLevel.AV1Level2); + AV1_LEVEL_NUMBER_TO_CONST.put(1, CodecProfileLevel.AV1Level21); + AV1_LEVEL_NUMBER_TO_CONST.put(2, CodecProfileLevel.AV1Level22); + AV1_LEVEL_NUMBER_TO_CONST.put(3, CodecProfileLevel.AV1Level23); + AV1_LEVEL_NUMBER_TO_CONST.put(4, CodecProfileLevel.AV1Level3); + AV1_LEVEL_NUMBER_TO_CONST.put(5, CodecProfileLevel.AV1Level31); + AV1_LEVEL_NUMBER_TO_CONST.put(6, CodecProfileLevel.AV1Level32); + AV1_LEVEL_NUMBER_TO_CONST.put(7, CodecProfileLevel.AV1Level33); + AV1_LEVEL_NUMBER_TO_CONST.put(8, CodecProfileLevel.AV1Level4); + AV1_LEVEL_NUMBER_TO_CONST.put(9, CodecProfileLevel.AV1Level41); + AV1_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AV1Level42); + AV1_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.AV1Level43); + AV1_LEVEL_NUMBER_TO_CONST.put(12, CodecProfileLevel.AV1Level5); + AV1_LEVEL_NUMBER_TO_CONST.put(13, CodecProfileLevel.AV1Level51); + AV1_LEVEL_NUMBER_TO_CONST.put(14, CodecProfileLevel.AV1Level52); + AV1_LEVEL_NUMBER_TO_CONST.put(15, CodecProfileLevel.AV1Level53); + AV1_LEVEL_NUMBER_TO_CONST.put(16, CodecProfileLevel.AV1Level6); + AV1_LEVEL_NUMBER_TO_CONST.put(17, CodecProfileLevel.AV1Level61); + AV1_LEVEL_NUMBER_TO_CONST.put(18, CodecProfileLevel.AV1Level62); + AV1_LEVEL_NUMBER_TO_CONST.put(19, CodecProfileLevel.AV1Level63); + AV1_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.AV1Level7); + AV1_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.AV1Level71); + AV1_LEVEL_NUMBER_TO_CONST.put(22, CodecProfileLevel.AV1Level72); + AV1_LEVEL_NUMBER_TO_CONST.put(23, CodecProfileLevel.AV1Level73); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE = new SparseIntArray(); MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(1, CodecProfileLevel.AACObjectMain); MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(2, CodecProfileLevel.AACObjectLC); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index e34b4074fb..0fc0a85104 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -34,12 +34,6 @@ import java.util.Arrays; */ public final class MetadataRenderer extends BaseRenderer implements Callback { - /** - * @deprecated Use {@link MetadataOutput}. - */ - @Deprecated - public interface Output extends MetadataOutput {} - private static final int MSG_INVOKE_RENDERER = 0; // TODO: Holding multiple pending metadata objects is temporary mitigation against // https://github.com/google/ExoPlayer/issues/1874. It should be removed once this issue has been @@ -48,7 +42,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private final MetadataDecoderFactory decoderFactory; private final MetadataOutput output; - private final @Nullable Handler outputHandler; + @Nullable private final Handler outputHandler; private final FormatHolder formatHolder; private final MetadataInputBuffer buffer; private final Metadata[] pendingMetadata; @@ -58,6 +52,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private int pendingMetadataCount; private MetadataDecoder decoder; private boolean inputStreamEnded; + private long subsampleOffsetUs; /** * @param output The output. @@ -126,7 +121,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { // If we ever need to support a metadata format where this is not the case, we'll need to // pass the buffer to the decoder and discard the output. } else { - buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; + buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.flip(); int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; Metadata metadata = decoder.decode(buffer); @@ -136,6 +131,8 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { pendingMetadataCount++; } } + } else if (result == C.RESULT_FORMAT_READ) { + subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index 33d79917eb..f592a6eee7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -19,43 +19,30 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.util.Arrays; -/** - * Decodes Event Message (emsg) atoms, as defined in ISO/IEC 23009-1:2014, Section 5.10.3.3. - * - *

Atom data should be provided to the decoder without the full atom header (i.e. starting from - * the first byte of the scheme_id_uri field). It is expected that the presentation_time_delta field - * should be 0, having already been accounted for by adjusting the sample timestamp. - */ +/** Decodes data encoded by {@link EventMessageEncoder}. */ public final class EventMessageDecoder implements MetadataDecoder { - private static final String TAG = "EventMessageDecoder"; - @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); - ParsableByteArray emsgData = new ParsableByteArray(data, size); + return new Metadata(decode(new ParsableByteArray(data, size))); + } + + public EventMessage decode(ParsableByteArray emsgData) { String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); - long timescale = emsgData.readUnsignedInt(); - long presentationTimeDelta = emsgData.readUnsignedInt(); - if (presentationTimeDelta != 0) { - // We expect the source to have accounted for presentation_time_delta by adjusting the sample - // timestamp and zeroing the field in the sample data. Log a warning if the field is non-zero. - Log.w(TAG, "Ignoring non-zero presentation_time_delta: " + presentationTimeDelta); - } - long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), 1000, timescale); + long durationMs = emsgData.readUnsignedInt(); long id = emsgData.readUnsignedInt(); - byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size); - return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData)); + byte[] messageData = + Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); + return new EventMessage(schemeIdUri, value, durationMs, id, messageData); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java index dd33d591a7..4fa3f71b32 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.metadata.emsg; -import androidx.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; @@ -40,15 +39,12 @@ public final class EventMessageEncoder { * @param eventMessage The event message to be encoded. * @return The serialized byte array. */ - @Nullable public byte[] encode(EventMessage eventMessage) { byteArrayOutputStream.reset(); try { writeNullTerminatedString(dataOutputStream, eventMessage.schemeIdUri); String nonNullValue = eventMessage.value != null ? eventMessage.value : ""; writeNullTerminatedString(dataOutputStream, nonNullValue); - writeUnsignedInt(dataOutputStream, 1000); // timescale - writeUnsignedInt(dataOutputStream, 0); // presentation_time_delta writeUnsignedInt(dataOutputStream, eventMessage.durationMs); writeUnsignedInt(dataOutputStream, eventMessage.id); dataOutputStream.write(eventMessage.messageData); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java new file mode 100644 index 0000000000..ce134614ad --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.metadata.flac; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; +import java.util.Arrays; + +/** A picture parsed from a FLAC file. */ +public final class PictureFrame implements Metadata.Entry { + + /** The type of the picture. */ + public final int pictureType; + /** The mime type of the picture. */ + public final String mimeType; + /** A description of the picture. */ + public final String description; + /** The width of the picture in pixels. */ + public final int width; + /** The height of the picture in pixels. */ + public final int height; + /** The color depth of the picture in bits-per-pixel. */ + public final int depth; + /** For indexed-color pictures (e.g. GIF), the number of colors used. 0 otherwise. */ + public final int colors; + /** The encoded picture data. */ + public final byte[] pictureData; + + public PictureFrame( + int pictureType, + String mimeType, + String description, + int width, + int height, + int depth, + int colors, + byte[] pictureData) { + this.pictureType = pictureType; + this.mimeType = mimeType; + this.description = description; + this.width = width; + this.height = height; + this.depth = depth; + this.colors = colors; + this.pictureData = pictureData; + } + + /* package */ PictureFrame(Parcel in) { + this.pictureType = in.readInt(); + this.mimeType = castNonNull(in.readString()); + this.description = castNonNull(in.readString()); + this.width = in.readInt(); + this.height = in.readInt(); + this.depth = in.readInt(); + this.colors = in.readInt(); + this.pictureData = castNonNull(in.createByteArray()); + } + + @Override + public String toString() { + return "Picture: mimeType=" + mimeType + ", description=" + description; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PictureFrame other = (PictureFrame) obj; + return (pictureType == other.pictureType) + && mimeType.equals(other.mimeType) + && description.equals(other.description) + && (width == other.width) + && (height == other.height) + && (depth == other.depth) + && (colors == other.colors) + && Arrays.equals(pictureData, other.pictureData); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + pictureType; + result = 31 * result + mimeType.hashCode(); + result = 31 * result + description.hashCode(); + result = 31 * result + width; + result = 31 * result + height; + result = 31 * result + depth; + result = 31 * result + colors; + result = 31 * result + Arrays.hashCode(pictureData); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(pictureType); + dest.writeString(mimeType); + dest.writeString(description); + dest.writeInt(width); + dest.writeInt(height); + dest.writeInt(depth); + dest.writeInt(colors); + dest.writeByteArray(pictureData); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public PictureFrame createFromParcel(Parcel in) { + return new PictureFrame(in); + } + + @Override + public PictureFrame[] newArray(int size) { + return new PictureFrame[size]; + } + }; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java new file mode 100644 index 0000000000..9f44cdf393 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java @@ -0,0 +1,99 @@ +/* + * 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.metadata.flac; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; + +/** A vorbis comment. */ +public final class VorbisComment implements Metadata.Entry { + + /** The key. */ + public final String key; + + /** The value. */ + public final String value; + + /** + * @param key The key. + * @param value The value. + */ + public VorbisComment(String key, String value) { + this.key = key; + this.value = value; + } + + /* package */ VorbisComment(Parcel in) { + this.key = castNonNull(in.readString()); + this.value = castNonNull(in.readString()); + } + + @Override + public String toString() { + return "VC: " + key + "=" + value; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + VorbisComment other = (VorbisComment) obj; + return key.equals(other.key) && value.equals(other.value); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + key.hashCode(); + result = 31 * result + value.hashCode(); + return result; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeString(value); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public VorbisComment createFromParcel(Parcel in) { + return new VorbisComment(in); + } + + @Override + public VorbisComment[] newArray(int size) { + return new VorbisComment[size]; + } + }; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index c233ad61b2..d4bedc63cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -31,7 +31,7 @@ public final class ApicFrame extends Id3Frame { public static final String ID = "APIC"; public final String mimeType; - public final @Nullable String description; + @Nullable public final String description; public final int pictureType; public final byte[] pictureData; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index fff0828b3a..85a59c3aeb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -61,10 +61,8 @@ public final class Id3Decoder implements MetadataDecoder { private static final String TAG = "Id3Decoder"; - /** - * The first three bytes of a well formed ID3 tag header. - */ - public static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + /** The first three bytes of a well formed ID3 tag header. */ + public static final int ID3_TAG = 0x00494433; /** * Length of an ID3 tag header. */ @@ -84,7 +82,7 @@ public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; - private final @Nullable FramePredicate framePredicate; + @Nullable private final FramePredicate framePredicate; public Id3Decoder() { this(null); @@ -99,7 +97,8 @@ public final class Id3Decoder implements MetadataDecoder { @SuppressWarnings("ByteBufferBackingArray") @Override - public @Nullable Metadata decode(MetadataInputBuffer inputBuffer) { + @Nullable + public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; return decode(buffer.array(), buffer.limit()); } @@ -112,7 +111,8 @@ public final class Id3Decoder implements MetadataDecoder { * @return A {@link Metadata} object containing the decoded ID3 tags, or null if the data could * not be decoded. */ - public @Nullable Metadata decode(byte[] data, int size) { + @Nullable + public Metadata decode(byte[] data, int size) { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index 8a36276b91..0e129ca7bb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.Util; */ public final class TextInformationFrame extends Id3Frame { - public final @Nullable String description; + @Nullable public final String description; public final String value; public TextInformationFrame(String id, @Nullable String description, String value) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java index 8be9ed1881..298558b662 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.Util; */ public final class UrlLinkFrame extends Id3Frame { - public final @Nullable String description; + @Nullable public final String description; public final String url; public UrlLinkFrame(String id, @Nullable String description, String url) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java index a053185435..c69908c746 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java @@ -68,7 +68,7 @@ import java.util.List; if (!exists()) { return new DownloadRequest[0]; } - InputStream inputStream = null; + @Nullable InputStream inputStream = null; try { inputStream = atomicFile.openRead(); DataInputStream dataInputStream = new DataInputStream(inputStream); @@ -99,7 +99,7 @@ import java.util.List; boolean isRemoveAction = input.readBoolean(); int dataLength = input.readInt(); - byte[] data; + @Nullable byte[] data; if (dataLength != 0) { data = new byte[dataLength]; input.readFully(data); @@ -123,7 +123,7 @@ import java.util.List; && (DownloadRequest.TYPE_DASH.equals(type) || DownloadRequest.TYPE_HLS.equals(type) || DownloadRequest.TYPE_SS.equals(type)); - String customCacheKey = null; + @Nullable String customCacheKey = null; if (!isLegacySegmented) { customCacheKey = input.readBoolean() ? input.readUTF() : null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java index baf47772ab..9ecce6e150 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -97,7 +97,7 @@ public final class ActionFileUpgradeUtil { boolean addNewDownloadAsCompleted, long nowMs) throws IOException { - Download download = downloadIndex.getDownload(request.id); + @Nullable Download download = downloadIndex.getDownload(request.id); if (download != null) { download = DownloadManager.mergeRequest(download, request, download.stopReason, nowMs); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index d2b7bd84d2..54360f8f6b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.offline; +import android.content.Context; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; @@ -31,6 +32,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; 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.MediaSourceFactory; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroup; @@ -81,12 +83,25 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; */ public final class DownloadHelper { + /** Default track selection parameters for downloading, but without any viewport constraints. */ + public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT = + Parameters.DEFAULT_WITHOUT_VIEWPORT.buildUpon().setForceHighestSupportedBitrate(true).build(); + /** - * The default parameters used for track selection for downloading. This default selects the - * highest bitrate audio and video tracks which are supported by the renderers. + * @deprecated This instance does not have viewport constraints configured for the primary + * display. Use {@link #getDefaultTrackSelectorParameters(Context)} instead. */ + @Deprecated public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS = - new DefaultTrackSelector.ParametersBuilder().setForceHighestSupportedBitrate(true).build(); + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT; + + /** Returns the default parameters used for track selection for downloading. */ + public static DefaultTrackSelector.Parameters getDefaultTrackSelectorParameters(Context context) { + return Parameters.getDefaults(context) + .buildUpon() + .setForceHighestSupportedBitrate(true) + .build(); + } /** A callback to be notified when the {@link DownloadHelper} is prepared. */ public interface Callback { @@ -119,12 +134,9 @@ public final class DownloadHelper { private static final Constructor HLS_FACTORY_CONSTRUCTOR = getConstructor("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); - /** - * Creates a {@link DownloadHelper} for progressive streams. - * - * @param uri A stream {@link Uri}. - * @return A {@link DownloadHelper} for progressive streams. - */ + /** @deprecated Use {@link #forProgressive(Context, Uri)} */ + @Deprecated + @SuppressWarnings("deprecation") public static DownloadHelper forProgressive(Uri uri) { return forProgressive(uri, /* cacheKey= */ null); } @@ -132,23 +144,60 @@ public final class DownloadHelper { /** * Creates a {@link DownloadHelper} for progressive streams. * + * @param context Any {@link Context}. * @param uri A stream {@link Uri}. - * @param cacheKey An optional cache key. * @return A {@link DownloadHelper} for progressive streams. */ + public static DownloadHelper forProgressive(Context context, Uri uri) { + return forProgressive(context, uri, /* cacheKey= */ null); + } + + /** @deprecated Use {@link #forProgressive(Context, Uri, String)} */ + @Deprecated public static DownloadHelper forProgressive(Uri uri, @Nullable String cacheKey) { return new DownloadHelper( DownloadRequest.TYPE_PROGRESSIVE, uri, cacheKey, /* mediaSource= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT, /* rendererCapabilities= */ new RendererCapabilities[0]); } + /** + * Creates a {@link DownloadHelper} for progressive streams. + * + * @param context Any {@link Context}. + * @param uri A stream {@link Uri}. + * @param cacheKey An optional cache key. + * @return A {@link DownloadHelper} for progressive streams. + */ + public static DownloadHelper forProgressive(Context context, Uri uri, @Nullable String cacheKey) { + return new DownloadHelper( + DownloadRequest.TYPE_PROGRESSIVE, + uri, + cacheKey, + /* mediaSource= */ null, + getDefaultTrackSelectorParameters(context), + /* rendererCapabilities= */ new RendererCapabilities[0]); + } + + /** @deprecated Use {@link #forDash(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forDash( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forDash( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); + } + /** * Creates a {@link DownloadHelper} for DASH streams. * + * @param context Any {@link Context}. * @param uri A manifest {@link Uri}. * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are @@ -157,13 +206,16 @@ public final class DownloadHelper { * @throws IllegalStateException If the DASH module is missing. */ public static DownloadHelper forDash( - Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { return forDash( uri, dataSourceFactory, renderersFactory, /* drmSessionManager= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS); + getDefaultTrackSelectorParameters(context)); } /** @@ -196,9 +248,22 @@ public final class DownloadHelper { Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } + /** @deprecated Use {@link #forHls(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forHls( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forHls( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); + } + /** * Creates a {@link DownloadHelper} for HLS streams. * + * @param context Any {@link Context}. * @param uri A playlist {@link Uri}. * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are @@ -207,13 +272,16 @@ public final class DownloadHelper { * @throws IllegalStateException If the HLS module is missing. */ public static DownloadHelper forHls( - Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { return forHls( uri, dataSourceFactory, renderersFactory, /* drmSessionManager= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS); + getDefaultTrackSelectorParameters(context)); } /** @@ -246,9 +314,22 @@ public final class DownloadHelper { Util.getRendererCapabilities(renderersFactory, drmSessionManager)); } + /** @deprecated Use {@link #forSmoothStreaming(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forSmoothStreaming( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forSmoothStreaming( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); + } + /** * Creates a {@link DownloadHelper} for SmoothStreaming streams. * + * @param context Any {@link Context}. * @param uri A manifest {@link Uri}. * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are @@ -257,13 +338,16 @@ public final class DownloadHelper { * @throws IllegalStateException If the SmoothStreaming module is missing. */ public static DownloadHelper forSmoothStreaming( - Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { return forSmoothStreaming( uri, dataSourceFactory, renderersFactory, /* drmSessionManager= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS); + getDefaultTrackSelectorParameters(context)); } /** @@ -306,7 +390,7 @@ public final class DownloadHelper { */ public static MediaSource createMediaSource( DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { - Constructor constructor; + @Nullable Constructor constructor; switch (downloadRequest.type) { case DownloadRequest.TYPE_DASH: constructor = DASH_FACTORY_CONSTRUCTOR; @@ -335,6 +419,7 @@ public final class DownloadHelper { private final RendererCapabilities[] rendererCapabilities; private final SparseIntArray scratchSet; private final Handler callbackHandler; + private final Timeline.Window window; private boolean isPreparedWithMedia; private @MonotonicNonNull Callback callback; @@ -368,12 +453,13 @@ public final class DownloadHelper { this.uri = uri; this.cacheKey = cacheKey; this.mediaSource = mediaSource; - this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory()); + this.trackSelector = + new DefaultTrackSelector(trackSelectorParameters, new DownloadTrackSelection.Factory()); this.rendererCapabilities = rendererCapabilities; this.scratchSet = new SparseIntArray(); - trackSelector.setParameters(trackSelectorParameters); trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); callbackHandler = new Handler(Util.getLooper()); + window = new Timeline.Window(); } /** @@ -409,7 +495,9 @@ public final class DownloadHelper { return null; } assertPreparedWithMedia(); - return mediaPreparer.manifest; + return mediaPreparer.timeline.getWindowCount() > 0 + ? mediaPreparer.timeline.getWindow(/* windowIndex= */ 0, window).manifest + : null; } /** @@ -720,7 +808,7 @@ public final class DownloadHelper { new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)), mediaPreparer.timeline); for (int i = 0; i < trackSelectorResult.length; i++) { - TrackSelection newSelection = trackSelectorResult.selections.get(i); + @Nullable TrackSelection newSelection = trackSelectorResult.selections.get(i); if (newSelection == null) { continue; } @@ -796,7 +884,7 @@ public final class DownloadHelper { } private static final class MediaPreparer - implements MediaSource.SourceInfoRefreshListener, MediaPeriod.Callback, Handler.Callback { + implements MediaSourceCaller, MediaPeriod.Callback, Handler.Callback { private static final int MESSAGE_PREPARE_SOURCE = 0; private static final int MESSAGE_CHECK_FOR_FAILURE = 1; @@ -809,12 +897,11 @@ public final class DownloadHelper { private final MediaSource mediaSource; private final DownloadHelper downloadHelper; private final Allocator allocator; + private final ArrayList pendingMediaPeriods; + private final Handler downloadHelperHandler; private final HandlerThread mediaSourceThread; private final Handler mediaSourceHandler; - private final Handler downloadHelperHandler; - private final ArrayList pendingMediaPeriods; - @Nullable public Object manifest; public @MonotonicNonNull Timeline timeline; public MediaPeriod @MonotonicNonNull [] mediaPeriods; @@ -824,6 +911,7 @@ public final class DownloadHelper { this.mediaSource = mediaSource; this.downloadHelper = downloadHelper; allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + pendingMediaPeriods = new ArrayList<>(); @SuppressWarnings("methodref.receiver.bound.invalid") Handler downloadThreadHandler = Util.createHandler(this::handleDownloadHelperCallbackMessage); this.downloadHelperHandler = downloadThreadHandler; @@ -831,7 +919,6 @@ public final class DownloadHelper { mediaSourceThread.start(); mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this); mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE); - pendingMediaPeriods = new ArrayList<>(); } public void release() { @@ -848,7 +935,7 @@ public final class DownloadHelper { public boolean handleMessage(Message msg) { switch (msg.what) { case MESSAGE_PREPARE_SOURCE: - mediaSource.prepareSource(/* listener= */ this, /* mediaTransferListener= */ null); + mediaSource.prepareSource(/* caller= */ this, /* mediaTransferListener= */ null); mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); return true; case MESSAGE_CHECK_FOR_FAILURE: @@ -889,17 +976,15 @@ public final class DownloadHelper { } } - // MediaSource.SourceInfoRefreshListener implementation. + // MediaSource.MediaSourceCaller implementation. @Override - public void onSourceInfoRefreshed( - MediaSource source, Timeline timeline, @Nullable Object manifest) { + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { if (this.timeline != null) { // Ignore dynamic updates. return; } this.timeline = timeline; - this.manifest = manifest; mediaPeriods = new MediaPeriod[timeline.getPeriodCount()]; for (int i = 0; i < mediaPeriods.length; i++) { MediaPeriod mediaPeriod = @@ -943,6 +1028,7 @@ public final class DownloadHelper { downloadHelper.onMediaPrepared(); return true; case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED: + release(); downloadHelper.onMediaPreparationFailed((IOException) Util.castNonNull(msg.obj)); return true; default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index ec5ff81d97..c3cf0bdc24 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -731,7 +731,7 @@ public final class DownloadManager { Log.e(TAG, "Failed to set manual stop reason", e); } } else { - Download download = getDownload(id, /* loadFromIndex= */ false); + @Nullable Download download = getDownload(id, /* loadFromIndex= */ false); if (download != null) { setStopReason(download, stopReason); } else { @@ -779,7 +779,7 @@ public final class DownloadManager { } private void addDownload(DownloadRequest request, int stopReason) { - Download download = getDownload(request.id, /* loadFromIndex= */ true); + @Nullable Download download = getDownload(request.id, /* loadFromIndex= */ true); long nowMs = System.currentTimeMillis(); if (download != null) { putDownload(mergeRequest(download, request, stopReason, nowMs)); @@ -798,7 +798,7 @@ public final class DownloadManager { } private void removeDownload(String id) { - Download download = getDownload(id, /* loadFromIndex= */ true); + @Nullable Download download = getDownload(id, /* loadFromIndex= */ true); if (download == null) { Log.e(TAG, "Failed to remove nonexistent download: " + id); return; @@ -860,7 +860,7 @@ public final class DownloadManager { int accumulatingDownloadTaskCount = 0; for (int i = 0; i < downloads.size(); i++) { Download download = downloads.get(i); - Task activeTask = activeTasks.get(download.request.id); + @Nullable Task activeTask = activeTasks.get(download.request.id); switch (download.state) { case STATE_STOPPED: syncStoppedDownload(activeTask); @@ -999,7 +999,7 @@ public final class DownloadManager { return; } - Throwable finalError = task.finalError; + @Nullable Throwable finalError = task.finalError; if (finalError != null) { Log.e(TAG, "Task failed: " + task.request + ", " + isRemove, finalError); } @@ -1176,7 +1176,7 @@ public final class DownloadManager { private final boolean isRemove; private final int minRetryCount; - private volatile InternalHandler internalHandler; + @Nullable private volatile InternalHandler internalHandler; private volatile boolean isCanceled; @Nullable private Throwable finalError; @@ -1246,7 +1246,7 @@ public final class DownloadManager { } catch (Throwable e) { finalError = e; } - Handler internalHandler = this.internalHandler; + @Nullable Handler internalHandler = this.internalHandler; if (internalHandler != null) { internalHandler.obtainMessage(MSG_TASK_STOPPED, this).sendToTarget(); } @@ -1258,7 +1258,7 @@ public final class DownloadManager { downloadProgress.percentDownloaded = percentDownloaded; if (contentLength != this.contentLength) { this.contentLength = contentLength; - Handler internalHandler = this.internalHandler; + @Nullable Handler internalHandler = this.internalHandler; if (internalHandler != null) { internalHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 3900dc8e93..db10517b67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -174,8 +174,9 @@ public abstract class DownloadService extends Service { @Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater; @Nullable private final String channelId; @StringRes private final int channelNameResourceId; + @StringRes private final int channelDescriptionResourceId; - private DownloadManager downloadManager; + @Nullable private DownloadManager downloadManager; private int lastStartId; private boolean startedInForeground; private boolean taskRemoved; @@ -214,7 +215,23 @@ public abstract class DownloadService extends Service { foregroundNotificationId, foregroundNotificationUpdateInterval, /* channelId= */ null, - /* channelNameResourceId= */ 0); + /* channelNameResourceId= */ 0, + /* channelDescriptionResourceId= */ 0); + } + + /** @deprecated Use {@link #DownloadService(int, long, String, int, int)}. */ + @Deprecated + protected DownloadService( + int foregroundNotificationId, + long foregroundNotificationUpdateInterval, + @Nullable String channelId, + @StringRes int channelNameResourceId) { + this( + foregroundNotificationId, + foregroundNotificationUpdateInterval, + channelId, + channelNameResourceId, + /* channelDescriptionResourceId= */ 0); } /** @@ -230,25 +247,33 @@ public abstract class DownloadService extends Service { * unique per package. The value may be truncated if it's too long. Ignored if {@code * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. * @param channelNameResourceId A string resource identifier for the user visible name of the - * channel, if {@code channelId} is specified. The recommended maximum length is 40 - * characters. The value may be truncated if it is too long. Ignored if {@code + * notification channel. The recommended maximum length is 40 characters. The value may be + * truncated if it's too long. Ignored if {@code channelId} is null or if {@code * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelDescriptionResourceId A string resource identifier for the user visible + * description of the notification channel, or 0 if no description is provided. The + * recommended maximum length is 300 characters. The value may be truncated if it is too long. + * Ignored if {@code channelId} is null or if {@code foregroundNotificationId} is {@link + * #FOREGROUND_NOTIFICATION_ID_NONE}. */ protected DownloadService( int foregroundNotificationId, long foregroundNotificationUpdateInterval, @Nullable String channelId, - @StringRes int channelNameResourceId) { + @StringRes int channelNameResourceId, + @StringRes int channelDescriptionResourceId) { if (foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE) { this.foregroundNotificationUpdater = null; this.channelId = null; this.channelNameResourceId = 0; + this.channelDescriptionResourceId = 0; } else { this.foregroundNotificationUpdater = new ForegroundNotificationUpdater( foregroundNotificationId, foregroundNotificationUpdateInterval); this.channelId = channelId; this.channelNameResourceId = channelNameResourceId; + this.channelDescriptionResourceId = channelDescriptionResourceId; } } @@ -543,7 +568,11 @@ public abstract class DownloadService extends Service { public void onCreate() { if (channelId != null) { NotificationUtil.createNotificationChannel( - this, channelId, channelNameResourceId, NotificationUtil.IMPORTANCE_LOW); + this, + channelId, + channelNameResourceId, + channelDescriptionResourceId, + NotificationUtil.IMPORTANCE_LOW); } Class clazz = getClass(); DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz); @@ -563,8 +592,8 @@ public abstract class DownloadService extends Service { public int onStartCommand(Intent intent, int flags, int startId) { lastStartId = startId; taskRemoved = false; - String intentAction = null; - String contentId = null; + @Nullable String intentAction = null; + @Nullable String contentId = null; if (intent != null) { intentAction = intent.getAction(); startedInForeground |= @@ -575,13 +604,14 @@ public abstract class DownloadService extends Service { if (intentAction == null) { intentAction = ACTION_INIT; } + DownloadManager downloadManager = Assertions.checkNotNull(this.downloadManager); switch (intentAction) { case ACTION_INIT: case ACTION_RESTART: // Do nothing. break; case ACTION_ADD_DOWNLOAD: - DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); + @Nullable DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); if (downloadRequest == null) { Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); } else { @@ -614,7 +644,7 @@ public abstract class DownloadService extends Service { } break; case ACTION_SET_REQUIREMENTS: - Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS); + @Nullable Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS); if (requirements == null) { Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); } else { @@ -640,8 +670,9 @@ public abstract class DownloadService extends Service { @Override public void onDestroy() { isDestroyed = true; - DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(getClass()); - boolean unschedule = !downloadManager.isWaitingForRequirements(); + DownloadManagerHelper downloadManagerHelper = + Assertions.checkNotNull(downloadManagerListeners.get(getClass())); + boolean unschedule = !downloadManagerHelper.downloadManager.isWaitingForRequirements(); downloadManagerHelper.detachService(this, unschedule); if (foregroundNotificationUpdater != null) { foregroundNotificationUpdater.stopPeriodicUpdates(); @@ -775,7 +806,6 @@ public abstract class DownloadService extends Service { private final int notificationId; private final long updateInterval; private final Handler handler; - private final Runnable updateRunnable; private boolean periodicUpdatesStarted; private boolean notificationDisplayed; @@ -784,7 +814,6 @@ public abstract class DownloadService extends Service { this.notificationId = notificationId; this.updateInterval = updateInterval; this.handler = new Handler(Looper.getMainLooper()); - this.updateRunnable = this::update; } public void startPeriodicUpdates() { @@ -794,7 +823,7 @@ public abstract class DownloadService extends Service { public void stopPeriodicUpdates() { periodicUpdatesStarted = false; - handler.removeCallbacks(updateRunnable); + handler.removeCallbacksAndMessages(null); } public void showNotificationIfNotAlready() { @@ -810,12 +839,12 @@ public abstract class DownloadService extends Service { } private void update() { - List downloads = downloadManager.getCurrentDownloads(); + List downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads(); startForeground(notificationId, getForegroundNotification(downloads)); notificationDisplayed = true; if (periodicUpdatesStarted) { - handler.removeCallbacks(updateRunnable); - handler.postDelayed(updateRunnable, updateInterval); + handler.removeCallbacksAndMessages(null); + handler.postDelayed(this::update, updateInterval); } } } @@ -840,7 +869,8 @@ public abstract class DownloadService extends Service { downloadManager.addListener(this); if (scheduler != null) { Requirements requirements = downloadManager.getRequirements(); - setSchedulerEnabled(/* enabled= */ !requirements.checkRequirements(context), requirements); + setSchedulerEnabled( + scheduler, /* enabled= */ !requirements.checkRequirements(context), requirements); } } @@ -894,11 +924,12 @@ public abstract class DownloadService extends Service { } } if (scheduler != null) { - setSchedulerEnabled(/* enabled= */ !requirementsMet, requirements); + setSchedulerEnabled(scheduler, /* enabled= */ !requirementsMet, requirements); } } - private void setSchedulerEnabled(boolean enabled, Requirements requirements) { + private void setSchedulerEnabled( + Scheduler scheduler, boolean enabled, Requirements requirements) { if (!enabled) { scheduler.cancel(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index 1643812ece..5326220452 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -138,7 +138,7 @@ public abstract class SegmentDownloader> impleme Collections.sort(segments); // Download the segments. - ProgressNotifier progressNotifier = null; + @Nullable ProgressNotifier progressNotifier = null; if (progressListener != null) { progressNotifier = new ProgressNotifier( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/package-info.java new file mode 100644 index 0000000000..61450c9cfd --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/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.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java index e6679e1a5a..752239c991 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.Intent; import android.os.PersistableBundle; import androidx.annotation.RequiresPermission; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -129,9 +130,8 @@ public final class PlatformScheduler implements Scheduler { logd("Requirements are met"); String serviceAction = extras.getString(KEY_SERVICE_ACTION); String servicePackage = extras.getString(KEY_SERVICE_PACKAGE); - // FIXME: incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") - Intent intent = new Intent(serviceAction).setPackage(servicePackage); + Intent intent = + new Intent(Assertions.checkNotNull(serviceAction)).setPackage(servicePackage); logd("Starting service action: " + serviceAction + " package: " + servicePackage); Util.startForegroundService(this, intent); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 30cf452572..35f8e37dcf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -27,6 +27,8 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.PowerManager; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -128,7 +130,7 @@ public final class Requirements implements Parcelable { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + NetworkInfo networkInfo = Assertions.checkNotNull(connectivityManager).getActiveNetworkInfo(); if (networkInfo == null || !networkInfo.isConnected() || !isInternetConnectivityValidated(connectivityManager)) { @@ -179,7 +181,7 @@ public final class Requirements implements Parcelable { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index f0d0f37cdf..0d9b8261d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -27,7 +27,9 @@ import android.net.NetworkRequest; import android.os.Handler; import android.os.Looper; import android.os.PowerManager; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; /** @@ -57,10 +59,10 @@ public final class RequirementsWatcher { private final Requirements requirements; private final Handler handler; - private DeviceStatusChangeReceiver receiver; + @Nullable private DeviceStatusChangeReceiver receiver; @Requirements.RequirementFlags private int notMetRequirements; - private CapabilityValidatedCallback networkCallback; + @Nullable private CapabilityValidatedCallback networkCallback; /** * @param context Any context. @@ -111,7 +113,7 @@ public final class RequirementsWatcher { /** Stops watching for changes. */ public void stop() { - context.unregisterReceiver(receiver); + context.unregisterReceiver(Assertions.checkNotNull(receiver)); receiver = null; if (networkCallback != null) { unregisterNetworkCallback(); @@ -126,7 +128,8 @@ public final class RequirementsWatcher { @TargetApi(23) private void registerNetworkCallbackV23() { ConnectivityManager connectivityManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + Assertions.checkNotNull( + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)); NetworkRequest request = new NetworkRequest.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) @@ -139,7 +142,7 @@ public final class RequirementsWatcher { if (Util.SDK_INT >= 21) { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - connectivityManager.unregisterNetworkCallback(networkCallback); + connectivityManager.unregisterNetworkCallback(Assertions.checkNotNull(networkCallback)); networkCallback = null; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 4a3505749a..db19764318 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -19,6 +19,7 @@ import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; /** * Abstract base class for the concatenation of one or more {@link Timeline}s. @@ -35,6 +36,7 @@ import com.google.android.exoplayer2.Timeline; * @param concatenatedUid UID of a period in a concatenated timeline. * @return UID of the child timeline this period belongs to. */ + @SuppressWarnings("nullness:return.type.incompatible") public static Object getChildTimelineUidFromConcatenatedUid(Object concatenatedUid) { return ((Pair) concatenatedUid).first; } @@ -45,6 +47,7 @@ import com.google.android.exoplayer2.Timeline; * @param concatenatedUid UID of a period in a concatenated timeline. * @return UID of the period in the child timeline. */ + @SuppressWarnings("nullness:return.type.incompatible") public static Object getChildPeriodUidFromConcatenatedUid(Object concatenatedUid) { return ((Pair) concatenatedUid).second; } @@ -220,7 +223,9 @@ import com.google.android.exoplayer2.Timeline; setIds); period.windowIndex += firstWindowIndexInChild; if (setIds) { - period.uid = getConcatenatedUid(getChildUidByChildIndex(childIndex), period.uid); + period.uid = + getConcatenatedUid( + getChildUidByChildIndex(childIndex), Assertions.checkNotNull(period.uid)); } return period; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java index f6ea3da089..86e00e0a37 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -22,30 +22,33 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.util.ArrayList; +import java.util.HashSet; /** * Base {@link MediaSource} implementation to handle parallel reuse and to keep a list of {@link * MediaSourceEventListener}s. * - *

Whenever an implementing subclass needs to provide a new timeline and/or manifest, it must - * call {@link #refreshSourceInfo(Timeline, Object)} to notify all listeners. + *

Whenever an implementing subclass needs to provide a new timeline, it must call {@link + * #refreshSourceInfo(Timeline)} to notify all listeners. */ public abstract class BaseMediaSource implements MediaSource { - private final ArrayList sourceInfoListeners; + private final ArrayList mediaSourceCallers; + private final HashSet enabledMediaSourceCallers; private final MediaSourceEventListener.EventDispatcher eventDispatcher; @Nullable private Looper looper; @Nullable private Timeline timeline; - @Nullable private Object manifest; public BaseMediaSource() { - sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1); + mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1); + enabledMediaSourceCallers = new HashSet<>(/* initialCapacity= */ 1); eventDispatcher = new MediaSourceEventListener.EventDispatcher(); } /** - * Starts source preparation. This method is called at most once until the next call to {@link + * Starts source preparation and enables the source, see {@link #prepareSource(MediaSourceCaller, + * TransferListener)}. This method is called at most once until the next call to {@link * #releaseSourceInternal()}. * * @param mediaTransferListener The transfer listener which should be informed of any media data @@ -55,9 +58,15 @@ public abstract class BaseMediaSource implements MediaSource { */ protected abstract void prepareSourceInternal(@Nullable TransferListener mediaTransferListener); + /** Enables the source, see {@link #enable(MediaSourceCaller)}. */ + protected void enableInternal() {} + + /** Disables the source, see {@link #disable(MediaSourceCaller)}. */ + protected void disableInternal() {} + /** - * Releases the source. This method is called exactly once after each call to {@link - * #prepareSourceInternal(TransferListener)}. + * Releases the source, see {@link #releaseSource(MediaSourceCaller)}. This method is called + * exactly once after each call to {@link #prepareSourceInternal(TransferListener)}. */ protected abstract void releaseSourceInternal(); @@ -65,13 +74,11 @@ public abstract class BaseMediaSource implements MediaSource { * Updates timeline and manifest and notifies all listeners of the update. * * @param timeline The new {@link Timeline}. - * @param manifest The new manifest. May be null. */ - protected final void refreshSourceInfo(Timeline timeline, @Nullable Object manifest) { + protected final void refreshSourceInfo(Timeline timeline) { this.timeline = timeline; - this.manifest = manifest; - for (SourceInfoRefreshListener listener : sourceInfoListeners) { - listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + for (MediaSourceCaller caller : mediaSourceCallers) { + caller.onSourceInfoRefreshed(/* source= */ this, timeline); } } @@ -118,6 +125,11 @@ public abstract class BaseMediaSource implements MediaSource { return eventDispatcher.withParameters(windowIndex, mediaPeriodId, mediaTimeOffsetMs); } + /** Returns whether the source is enabled. */ + protected final boolean isEnabled() { + return !enabledMediaSourceCallers.isEmpty(); + } + @Override public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) { eventDispatcher.addEventListener(handler, eventListener); @@ -130,27 +142,50 @@ public abstract class BaseMediaSource implements MediaSource { @Override public final void prepareSource( - SourceInfoRefreshListener listener, - @Nullable TransferListener mediaTransferListener) { + MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { Looper looper = Looper.myLooper(); Assertions.checkArgument(this.looper == null || this.looper == looper); - sourceInfoListeners.add(listener); + Timeline timeline = this.timeline; + mediaSourceCallers.add(caller); if (this.looper == null) { this.looper = looper; + enabledMediaSourceCallers.add(caller); prepareSourceInternal(mediaTransferListener); } else if (timeline != null) { - listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + enable(caller); + caller.onSourceInfoRefreshed(/* source= */ this, timeline); } } @Override - public final void releaseSource(SourceInfoRefreshListener listener) { - sourceInfoListeners.remove(listener); - if (sourceInfoListeners.isEmpty()) { + public final void enable(MediaSourceCaller caller) { + Assertions.checkNotNull(looper); + boolean wasDisabled = enabledMediaSourceCallers.isEmpty(); + enabledMediaSourceCallers.add(caller); + if (wasDisabled) { + enableInternal(); + } + } + + @Override + public final void disable(MediaSourceCaller caller) { + boolean wasEnabled = !enabledMediaSourceCallers.isEmpty(); + enabledMediaSourceCallers.remove(caller); + if (wasEnabled && enabledMediaSourceCallers.isEmpty()) { + disableInternal(); + } + } + + @Override + public final void releaseSource(MediaSourceCaller caller) { + mediaSourceCallers.remove(caller); + if (mediaSourceCallers.isEmpty()) { looper = null; timeline = null; - manifest = null; + enabledMediaSourceCallers.clear(); releaseSourceInternal(); + } else { + disable(caller); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index c078053110..d57dccd8fe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -25,6 +26,7 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their @@ -37,8 +39,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb */ public final MediaPeriod mediaPeriod; - private MediaPeriod.Callback callback; - private ClippingSampleStream[] sampleStreams; + @Nullable private MediaPeriod.Callback callback; + private @NullableType ClippingSampleStream[] sampleStreams; private long pendingInitialDiscontinuityPositionUs; /* package */ long startUs; /* package */ long endUs; @@ -95,10 +97,14 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { sampleStreams = new ClippingSampleStream[streams.length]; - SampleStream[] childStreams = new SampleStream[streams.length]; + @NullableType SampleStream[] childStreams = new SampleStream[streams.length]; for (int i = 0; i < streams.length; i++) { sampleStreams[i] = (ClippingSampleStream) streams[i]; childStreams[i] = sampleStreams[i] != null ? sampleStreams[i].childStream : null; @@ -119,7 +125,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb for (int i = 0; i < streams.length; i++) { if (childStreams[i] == null) { sampleStreams[i] = null; - } else if (streams[i] == null || sampleStreams[i].childStream != childStreams[i]) { + } else if (sampleStreams[i] == null || sampleStreams[i].childStream != childStreams[i]) { sampleStreams[i] = new ClippingSampleStream(childStreams[i]); } streams[i] = sampleStreams[i]; @@ -209,12 +215,12 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public void onPrepared(MediaPeriod mediaPeriod) { - callback.onPrepared(this); + Assertions.checkNotNull(callback).onPrepared(this); } @Override public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } /* package */ boolean isPendingInitialDiscontinuity() { @@ -238,7 +244,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } } - private static boolean shouldKeepInitialDiscontinuity(long startUs, TrackSelection[] selections) { + private static boolean shouldKeepInitialDiscontinuity( + long startUs, @NullableType TrackSelection[] selections) { // If the clipping start position is non-zero, the clipping sample streams will adjust // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer // timestamps can be negative, because sample streams provide buffers starting at a key-frame, @@ -300,7 +307,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } int result = childStream.readData(formatHolder, buffer, requireFormat); if (result == C.RESULT_FORMAT_READ) { - Format format = formatHolder.format; + Format format = Assertions.checkNotNull(formatHolder.format); if (format.encoderDelay != 0 || format.encoderPadding != 0) { // Clear gapless playback metadata if the start/end points don't match the media. int encoderDelay = startUs != 0 ? 0 : format.encoderDelay; @@ -328,7 +335,5 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } return childStream.skipData(positionUs); } - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index ce6254e975..81169354de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; + import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -86,9 +87,8 @@ public final class ClippingMediaSource extends CompositeMediaSource { private final ArrayList mediaPeriods; private final Timeline.Window window; - private @Nullable Object manifest; - private ClippingTimeline clippingTimeline; - private IllegalClippingException clippingError; + @Nullable private ClippingTimeline clippingTimeline; + @Nullable private IllegalClippingException clippingError; private long periodStartUs; private long periodEndUs; @@ -192,7 +192,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); prepareChildSource(/* id= */ null, mediaSource); } @@ -222,24 +222,22 @@ public final class ClippingMediaSource extends CompositeMediaSource { Assertions.checkState(mediaPeriods.remove(mediaPeriod)); mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); if (mediaPeriods.isEmpty() && !allowDynamicClippingUpdates) { - refreshClippedTimeline(clippingTimeline.timeline); + refreshClippedTimeline(Assertions.checkNotNull(clippingTimeline).timeline); } } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); clippingError = null; clippingTimeline = null; } @Override - protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) { + protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) { if (clippingError != null) { return; } - this.manifest = manifest; refreshClippedTimeline(timeline); } @@ -279,7 +277,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { clippingError = e; return; } - refreshSourceInfo(clippingTimeline, manifest); + refreshSourceInfo(clippingTimeline); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index 9323f7505c..4ebe97313b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -34,17 +34,17 @@ public abstract class CompositeMediaSource extends BaseMediaSource { private final HashMap childSources; - private @Nullable Handler eventHandler; - private @Nullable TransferListener mediaTransferListener; + @Nullable private Handler eventHandler; + @Nullable private TransferListener mediaTransferListener; - /** Create composite media source without child sources. */ + /** Creates composite media source without child sources. */ protected CompositeMediaSource() { childSources = new HashMap<>(); } @Override @CallSuper - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; eventHandler = new Handler(); } @@ -59,9 +59,25 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @Override @CallSuper - public void releaseSourceInternal() { + protected void enableInternal() { for (MediaSourceAndListener childSource : childSources.values()) { - childSource.mediaSource.releaseSource(childSource.listener); + childSource.mediaSource.enable(childSource.caller); + } + } + + @Override + @CallSuper + protected void disableInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.disable(childSource.caller); + } + } + + @Override + @CallSuper + protected void releaseSourceInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.releaseSource(childSource.caller); childSource.mediaSource.removeEventListener(childSource.eventListener); } childSources.clear(); @@ -73,32 +89,53 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * @param id The unique id used to prepare the child source. * @param mediaSource The child source whose source info has been refreshed. * @param timeline The timeline of the child source. - * @param manifest The manifest of the child source. */ protected abstract void onChildSourceInfoRefreshed( - T id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest); + T id, MediaSource mediaSource, Timeline timeline); /** * Prepares a child source. * - *

{@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline, Object)} will be called - * when the child source updates its timeline and/or manifest with the same {@code id} passed to - * this method. + *

{@link #onChildSourceInfoRefreshed(T, MediaSource, Timeline)} will be called when the child + * source updates its timeline with the same {@code id} passed to this method. * - *

Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)} - * will be released in {@link #releaseSourceInternal()}. + *

Any child sources that aren't explicitly released with {@link #releaseChildSource(T)} will + * be released in {@link #releaseSourceInternal()}. * * @param id A unique id to identify the child source preparation. Null is allowed as an id. * @param mediaSource The child {@link MediaSource}. */ protected final void prepareChildSource(final T id, MediaSource mediaSource) { Assertions.checkArgument(!childSources.containsKey(id)); - SourceInfoRefreshListener sourceListener = - (source, timeline, manifest) -> onChildSourceInfoRefreshed(id, source, timeline, manifest); + MediaSourceCaller caller = + (source, timeline) -> onChildSourceInfoRefreshed(id, source, timeline); MediaSourceEventListener eventListener = new ForwardingEventListener(id); - childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener)); + childSources.put(id, new MediaSourceAndListener(mediaSource, caller, eventListener)); mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); - mediaSource.prepareSource(sourceListener, mediaTransferListener); + mediaSource.prepareSource(caller, mediaTransferListener); + if (!isEnabled()) { + mediaSource.disable(caller); + } + } + + /** + * Enables a child source. + * + * @param id The unique id used to prepare the child source. + */ + protected final void enableChildSource(final T id) { + MediaSourceAndListener enabledChild = Assertions.checkNotNull(childSources.get(id)); + enabledChild.mediaSource.enable(enabledChild.caller); + } + + /** + * Disables a child source. + * + * @param id The unique id used to prepare the child source. + */ + protected final void disableChildSource(final T id) { + MediaSourceAndListener disabledChild = Assertions.checkNotNull(childSources.get(id)); + disabledChild.mediaSource.disable(disabledChild.caller); } /** @@ -108,7 +145,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { */ protected final void releaseChildSource(T id) { MediaSourceAndListener removedChild = Assertions.checkNotNull(childSources.remove(id)); - removedChild.mediaSource.releaseSource(removedChild.listener); + removedChild.mediaSource.releaseSource(removedChild.caller); removedChild.mediaSource.removeEventListener(removedChild.eventListener); } @@ -151,18 +188,28 @@ public abstract class CompositeMediaSource extends BaseMediaSource { return mediaTimeMs; } + /** + * Returns whether {@link MediaSourceEventListener#onMediaPeriodCreated(int, MediaPeriodId)} and + * {@link MediaSourceEventListener#onMediaPeriodReleased(int, MediaPeriodId)} events of the given + * media period should be reported. The default implementation is to always report these events. + * + * @param mediaPeriodId A {@link MediaPeriodId} in the composite media source. + * @return Whether create and release events for this media period should be reported. + */ + protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) { + return true; + } + private static final class MediaSourceAndListener { public final MediaSource mediaSource; - public final SourceInfoRefreshListener listener; + public final MediaSourceCaller caller; public final MediaSourceEventListener eventListener; public MediaSourceAndListener( - MediaSource mediaSource, - SourceInfoRefreshListener listener, - MediaSourceEventListener eventListener) { + MediaSource mediaSource, MediaSourceCaller caller, MediaSourceEventListener eventListener) { this.mediaSource = mediaSource; - this.listener = listener; + this.caller = caller; this.eventListener = eventListener; } } @@ -180,14 +227,20 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @Override public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodCreated(); + if (shouldDispatchCreateOrReleaseEvent( + Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) { + eventDispatcher.mediaPeriodCreated(); + } } } @Override public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodReleased(); + if (shouldDispatchCreateOrReleaseEvent( + Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) { + eventDispatcher.mediaPeriodReleased(); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index e73fdd58a3..8dfea1e511 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -18,9 +18,7 @@ package com.google.android.exoplayer2.source; import android.os.Handler; import android.os.Message; import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; @@ -37,6 +35,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -46,7 +45,7 @@ import java.util.Set; * during playback. It is valid for the same {@link MediaSource} instance to be present more than * once in the concatenation. Access to this class is thread-safe. */ -public class ConcatenatingMediaSource extends CompositeMediaSource { +public final class ConcatenatingMediaSource extends CompositeMediaSource { private static final int MSG_ADD = 0; private static final int MSG_REMOVE = 1; @@ -70,16 +69,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders; private final Map mediaSourceByMediaPeriod; private final Map mediaSourceByUid; + private final Set enabledMediaSourceHolders; private final boolean isAtomic; private final boolean useLazyPreparation; - private final Timeline.Window window; - private final Timeline.Period period; private boolean timelineUpdateScheduled; private Set nextTimelineUpdateOnCompletionActions; private ShuffleOrder shuffleOrder; - private int windowCount; - private int periodCount; /** * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same @@ -137,10 +133,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); this.nextTimelineUpdateOnCompletionActions = new HashSet<>(); this.pendingOnCompletionActions = new HashSet<>(); + this.enabledMediaSourceHolders = new HashSet<>(); this.isAtomic = isAtomic; this.useLazyPreparation = useLazyPreparation; - window = new Timeline.Window(); - period = new Timeline.Period(); addMediaSources(Arrays.asList(mediaSources)); } @@ -149,7 +144,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources) { + public synchronized void addMediaSources(Collection mediaSources) { addPublicMediaSources( mediaSourcesPublic.size(), mediaSources, @@ -221,7 +216,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, Handler handler, Runnable onCompletionAction) { addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction); } @@ -234,7 +229,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources) { + public synchronized void addMediaSources(int index, Collection mediaSources) { addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null); } @@ -249,7 +244,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, Handler handler, @@ -268,9 +263,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders = new ArrayList<>(mediaSources.size()); for (MediaSource mediaSource : mediaSources) { - mediaSourceHolders.add(new MediaSourceHolder(mediaSource)); + mediaSourceHolders.add(new MediaSourceHolder(mediaSource, useLazyPreparation)); } mediaSourcesPublic.addAll(index, mediaSourceHolders); if (playbackThreadHandler != null && !mediaSources.isEmpty()) { @@ -702,10 +704,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource onCompletionActions = nextTimelineUpdateOnCompletionActions; nextTimelineUpdateOnCompletionActions = new HashSet<>(); - refreshSourceInfo( - new ConcatenatedTimeline( - mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), - /* manifest= */ null); + refreshSourceInfo(new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic)); getPlaybackThreadHandlerOnPlaybackThread() .obtainMessage(MSG_ON_COMPLETION, onCompletionActions) .sendToTarget(); @@ -736,24 +735,21 @@ public class ConcatenatingMediaSource extends CompositeMediaSource 0) { MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); + Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); newMediaSourceHolder.reset( - newIndex, - previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(), - previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount()); + newIndex, previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount()); } else { - newMediaSourceHolder.reset( - newIndex, /* firstWindowIndexInChild= */ 0, /* firstPeriodIndexInChild= */ 0); + newMediaSourceHolder.reset(newIndex, /* firstWindowIndexInChild= */ 0); } - correctOffsets( - newIndex, - /* childIndexUpdate= */ 1, - newMediaSourceHolder.timeline.getWindowCount(), - newMediaSourceHolder.timeline.getPeriodCount()); + Timeline newTimeline = newMediaSourceHolder.mediaSource.getTimeline(); + correctOffsets(newIndex, /* childIndexUpdate= */ 1, newTimeline.getWindowCount()); mediaSourceHolders.add(newIndex, newMediaSourceHolder); mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder); - if (!useLazyPreparation) { - newMediaSourceHolder.hasStartedPreparing = true; - prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); + prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); + if (isEnabled() && mediaSourceByMediaPeriod.isEmpty()) { + enabledMediaSourceHolders.add(newMediaSourceHolder); + } else { + disableChildSource(newMediaSourceHolder); } } @@ -761,79 +757,24 @@ public class ConcatenatingMediaSource extends CompositeMediaSource periodPosition = - timeline.getPeriodPosition(window, period, /* windowIndex= */ 0, windowStartPositionUs); - Object periodUid = periodPosition.first; - long periodPositionUs = periodPosition.second; - mediaSourceHolder.timeline = DeferredTimeline.createWithRealTimeline(timeline, periodUid); - if (deferredMediaPeriod != null) { - deferredMediaPeriod.overridePreparePositionUs(periodPositionUs); - MediaPeriodId idInSource = - deferredMediaPeriod.id.copyWithPeriodUid( - getChildPeriodUid(mediaSourceHolder, deferredMediaPeriod.id.periodUid)); - deferredMediaPeriod.createPeriod(idInSource); + if (mediaSourceHolder.childIndex + 1 < mediaSourceHolders.size()) { + MediaSourceHolder nextHolder = mediaSourceHolders.get(mediaSourceHolder.childIndex + 1); + int windowOffsetUpdate = + timeline.getWindowCount() + - (nextHolder.firstWindowIndexInChild - mediaSourceHolder.firstWindowIndexInChild); + if (windowOffsetUpdate != 0) { + correctOffsets( + mediaSourceHolder.childIndex + 1, /* childIndexUpdate= */ 0, windowOffsetUpdate); } } - mediaSourceHolder.isPrepared = true; scheduleTimelineUpdate(); } private void removeMediaSourceInternal(int index) { MediaSourceHolder holder = mediaSourceHolders.remove(index); mediaSourceByUid.remove(holder.uid); - Timeline oldTimeline = holder.timeline; - correctOffsets( - index, - /* childIndexUpdate= */ -1, - -oldTimeline.getWindowCount(), - -oldTimeline.getPeriodCount()); + Timeline oldTimeline = holder.mediaSource.getTimeline(); + correctOffsets(index, /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount()); holder.isRemoved = true; maybeReleaseChildSource(holder); } @@ -842,91 +783,85 @@ public class ConcatenatingMediaSource extends CompositeMediaSource iterator = enabledMediaSourceHolders.iterator(); + while (iterator.hasNext()) { + MediaSourceHolder holder = iterator.next(); + if (holder.activeMediaPeriodIds.isEmpty()) { + disableChildSource(holder); + iterator.remove(); + } + } + } + /** Return uid of media source holder from period uid of concatenated source. */ private static Object getMediaSourceHolderUid(Object periodUid) { return ConcatenatedTimeline.getChildTimelineUidFromConcatenatedUid(periodUid); } /** Return uid of child period from period uid of concatenated source. */ - private static Object getChildPeriodUid(MediaSourceHolder holder, Object periodUid) { - Object childUid = ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); - return childUid.equals(DeferredTimeline.DUMMY_ID) ? holder.timeline.replacedId : childUid; + private static Object getChildPeriodUid(Object periodUid) { + return ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); } private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) { - if (holder.timeline.replacedId.equals(childPeriodUid)) { - childPeriodUid = DeferredTimeline.DUMMY_ID; - } return ConcatenatedTimeline.getConcatenatedUid(holder.uid, childPeriodUid); } /** Data class to hold playlist media sources together with meta data needed to process them. */ - /* package */ static final class MediaSourceHolder implements Comparable { + /* package */ static final class MediaSourceHolder { - public final MediaSource mediaSource; + public final MaskingMediaSource mediaSource; public final Object uid; - public final List activeMediaPeriods; + public final List activeMediaPeriodIds; - public DeferredTimeline timeline; public int childIndex; public int firstWindowIndexInChild; - public int firstPeriodIndexInChild; - public boolean hasStartedPreparing; - public boolean isPrepared; public boolean isRemoved; - public MediaSourceHolder(MediaSource mediaSource) { - this.mediaSource = mediaSource; - this.timeline = DeferredTimeline.createWithDummyTimeline(mediaSource.getTag()); - this.activeMediaPeriods = new ArrayList<>(); + public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) { + this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation); + this.activeMediaPeriodIds = new ArrayList<>(); this.uid = new Object(); } - public void reset(int childIndex, int firstWindowIndexInChild, int firstPeriodIndexInChild) { + public void reset(int childIndex, int firstWindowIndexInChild) { this.childIndex = childIndex; this.firstWindowIndexInChild = firstWindowIndexInChild; - this.firstPeriodIndexInChild = firstPeriodIndexInChild; - this.hasStartedPreparing = false; - this.isPrepared = false; this.isRemoved = false; - this.activeMediaPeriods.clear(); - } - - @Override - public int compareTo(@NonNull MediaSourceHolder other) { - return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild; + this.activeMediaPeriodIds.clear(); } } @@ -957,13 +892,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders, - int windowCount, - int periodCount, ShuffleOrder shuffleOrder, boolean isAtomic) { super(isAtomic, shuffleOrder); - this.windowCount = windowCount; - this.periodCount = periodCount; int childCount = mediaSourceHolders.size(); firstPeriodInChildIndices = new int[childCount]; firstWindowInChildIndices = new int[childCount]; @@ -971,13 +902,19 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); int index = 0; + int windowCount = 0; + int periodCount = 0; for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - timelines[index] = mediaSourceHolder.timeline; - firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild; - firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild; + timelines[index] = mediaSourceHolder.mediaSource.getTimeline(); + firstWindowInChildIndices[index] = windowCount; + firstPeriodInChildIndices[index] = periodCount; + windowCount += timelines[index].getWindowCount(); + periodCount += timelines[index].getPeriodCount(); uids[index] = mediaSourceHolder.uid; childIndexByUid.put(uids[index], index++); } + this.windowCount = windowCount; + this.periodCount = periodCount; } @Override @@ -1027,135 +964,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource sessionManager; + private final FormatHolder formatHolder; + private final boolean playClearSamplesWithoutKeys; + private @MonotonicNonNull Format currentFormat; + @Nullable private DrmSession currentSession; + + /** + * Creates a sample queue reader. + * + * @param upstream The {@link SampleQueue} from which the created reader will read samples. + * @param sessionManager The {@link DrmSessionManager} that will provide {@link DrmSession + * DrmSessions} for the encrypted regions. + */ + public DecryptableSampleQueueReader(SampleQueue upstream, DrmSessionManager sessionManager) { + this.upstream = upstream; + this.sessionManager = sessionManager; + formatHolder = new FormatHolder(); + playClearSamplesWithoutKeys = + (sessionManager.getFlags() & DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS) != 0; + } + + /** Releases any resources acquired by this reader. */ + public void release() { + if (currentSession != null) { + currentSession.releaseReference(); + currentSession = null; + } + } + + /** + * Throws an error that's preventing data from being read. Does nothing if no such error exists. + * + * @throws IOException The underlying error. + */ + public void maybeThrowError() throws IOException { + // TODO: Avoid throwing if the DRM error is not preventing a read operation. + if (currentSession != null && currentSession.getState() == DrmSession.STATE_ERROR) { + throw Assertions.checkNotNull(currentSession.getError()); + } + } + + /** + * Reads from the upstream {@link SampleQueue}, populating {@link FormatHolder#drmSession} if the + * current {@link Format#drmInitData} is not null. + * + *

This reader guarantees that any read results are usable by clients. An encrypted sample will + * only be returned along with a {@link FormatHolder#drmSession} that has available keys. + * + * @param outputFormatHolder A {@link FormatHolder} to populate in the case of reading a format. + * {@link FormatHolder#drmSession} will be populated if the read format's {@link + * Format#drmInitData} is not null. + * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the + * end of the stream. If the end of the stream has been reached, the {@link + * C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. If a {@link + * DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, only the buffer flags may be + * populated by this method and the read position of the queue will not change. + * @param formatRequired Whether the caller requires that the format of the stream be read even if + * it's not changing. A sample will never be read if set to true, however it is still possible + * for the end of stream or nothing to be read. + * @param loadingFinished True if an empty queue should be considered the end of the stream. + * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will + * be set if the buffer's timestamp is less than this value. + * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or + * {@link C#RESULT_BUFFER_READ}. + */ + @SuppressWarnings("ReferenceEquality") + public int read( + FormatHolder outputFormatHolder, + DecoderInputBuffer buffer, + boolean formatRequired, + boolean loadingFinished, + long decodeOnlyUntilUs) { + + boolean readFlagFormatRequired = false; + boolean readFlagAllowOnlyClearBuffers = false; + boolean onlyPropagateFormatChanges = false; + + if (currentFormat == null || formatRequired) { + readFlagFormatRequired = true; + } else if (sessionManager != DrmSessionManager.DUMMY + && currentFormat.drmInitData != null + && Assertions.checkNotNull(currentSession).getState() + != DrmSession.STATE_OPENED_WITH_KEYS) { + if (playClearSamplesWithoutKeys) { + // Content is encrypted and keys are not available, but clear samples are ok for reading. + readFlagAllowOnlyClearBuffers = true; + } else { + // We must not read any samples, but we may still read a format or the end of stream. + // However, because the formatRequired argument is false, we should not propagate a read + // format unless it is different than the current format. + onlyPropagateFormatChanges = true; + readFlagFormatRequired = true; + } + } + + int result = + upstream.read( + formatHolder, + buffer, + readFlagFormatRequired, + readFlagAllowOnlyClearBuffers, + loadingFinished, + decodeOnlyUntilUs); + if (result == C.RESULT_FORMAT_READ) { + if (onlyPropagateFormatChanges && currentFormat == formatHolder.format) { + return C.RESULT_NOTHING_READ; + } + onFormat(Assertions.checkNotNull(formatHolder.format), outputFormatHolder); + } + return result; + } + + /** + * Updates the current format and manages any necessary DRM resources. + * + * @param format The format read from upstream. + * @param outputFormatHolder The output {@link FormatHolder}. + */ + private void onFormat(Format format, FormatHolder outputFormatHolder) { + outputFormatHolder.format = format; + DrmInitData oldDrmInitData = currentFormat != null ? currentFormat.drmInitData : null; + currentFormat = format; + if (sessionManager == DrmSessionManager.DUMMY) { + // Avoid attempting to acquire a session using the dummy DRM session manager. It's likely that + // the media source creation has not yet been migrated and the renderer can acquire the + // session for the read DRM init data. + // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. + return; + } + outputFormatHolder.includesDrmSession = true; + outputFormatHolder.drmSession = currentSession; + if (Util.areEqual(oldDrmInitData, format.drmInitData)) { + // Nothing to do. + return; + } + // Ensure we acquire the new session before releasing the previous one in case the same session + // can be used for both DrmInitData. + DrmSession previousSession = currentSession; + DrmInitData drmInitData = currentFormat.drmInitData; + if (drmInitData != null) { + currentSession = + sessionManager.acquireSession(Assertions.checkNotNull(Looper.myLooper()), drmInitData); + } else { + currentSession = null; + } + outputFormatHolder.drmSession = currentSession; + + if (previousSession != null) { + previousSession.releaseReference(); + } + } + + /** Returns whether there is data available for reading. */ + public boolean isReady(boolean loadingFinished) { + @SampleQueue.PeekResult int nextInQueue = upstream.peekNext(); + if (nextInQueue == SampleQueue.PEEK_RESULT_NOTHING) { + return loadingFinished; + } else if (nextInQueue == SampleQueue.PEEK_RESULT_FORMAT) { + return true; + } else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_CLEAR) { + return currentSession == null || playClearSamplesWithoutKeys; + } else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_ENCRYPTED) { + return sessionManager == DrmSessionManager.DUMMY + || Assertions.checkNotNull(currentSession).getState() + == DrmSession.STATE_OPENED_WITH_KEYS; + } else { + throw new IllegalStateException(); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java deleted file mode 100644 index 1f77cae20d..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2017 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.source; - -/** @deprecated Use {@link ConcatenatingMediaSource} instead. */ -@Deprecated -public final class DynamicConcatenatingMediaSource extends ConcatenatingMediaSource { - - /** - * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(MediaSource...)} - * instead. - */ - @Deprecated - public DynamicConcatenatingMediaSource() {} - - /** - * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, - * MediaSource...)} instead. - */ - @Deprecated - public DynamicConcatenatingMediaSource(boolean isAtomic) { - super(isAtomic); - } - - /** - * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, ShuffleOrder, - * MediaSource...)} instead. - */ - @Deprecated - public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) { - super(isAtomic, shuffleOrder); - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 3951dc20a2..ee731cbc09 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; @@ -35,8 +36,7 @@ import java.io.IOException; /** @deprecated Use {@link ProgressiveMediaSource} instead. */ @Deprecated @SuppressWarnings("deprecation") -public final class ExtractorMediaSource extends BaseMediaSource - implements MediaSource.SourceInfoRefreshListener { +public final class ExtractorMediaSource extends CompositeMediaSource { /** @deprecated Use {@link MediaSourceEventListener} instead. */ @Deprecated @@ -58,15 +58,15 @@ public final class ExtractorMediaSource extends BaseMediaSource } - /** Use {@link ProgressiveMediaSource.Factory} instead. */ + /** @deprecated Use {@link ProgressiveMediaSource.Factory} instead. */ @Deprecated public static final class Factory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; - private @Nullable ExtractorsFactory extractorsFactory; - private @Nullable String customCacheKey; - private @Nullable Object tag; + @Nullable private ExtractorsFactory extractorsFactory; + @Nullable private String customCacheKey; + @Nullable private Object tag; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; private boolean isCreateCalled; @@ -221,6 +221,9 @@ public final class ExtractorMediaSource extends BaseMediaSource } } + /** + * @deprecated Use {@link ProgressiveMediaSource#DEFAULT_LOADING_CHECK_INTERVAL_BYTES} instead. + */ @Deprecated public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES; @@ -242,8 +245,8 @@ public final class ExtractorMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - Handler eventHandler, - EventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); } @@ -264,9 +267,9 @@ public final class ExtractorMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - Handler eventHandler, - EventListener eventListener, - String customCacheKey) { + @Nullable Handler eventHandler, + @Nullable EventListener eventListener, + @Nullable String customCacheKey) { this( uri, dataSourceFactory, @@ -296,9 +299,9 @@ public final class ExtractorMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - Handler eventHandler, - EventListener eventListener, - String customCacheKey, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener, + @Nullable String customCacheKey, int continueLoadingCheckIntervalBytes) { this( uri, @@ -326,6 +329,7 @@ public final class ExtractorMediaSource extends BaseMediaSource uri, dataSourceFactory, extractorsFactory, + DrmSessionManager.getDummyDrmSessionManager(), loadableLoadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, @@ -339,13 +343,15 @@ public final class ExtractorMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { - progressiveMediaSource.prepareSource(/* listener= */ this, mediaTransferListener); + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + prepareChildSource(/* id= */ null, progressiveMediaSource); } @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - progressiveMediaSource.maybeThrowSourceInfoRefreshError(); + protected void onChildSourceInfoRefreshed( + @Nullable Void id, MediaSource mediaSource, Timeline timeline) { + refreshSourceInfo(timeline); } @Override @@ -358,17 +364,6 @@ public final class ExtractorMediaSource extends BaseMediaSource progressiveMediaSource.releasePeriod(mediaPeriod); } - @Override - public void releaseSourceInternal() { - progressiveMediaSource.releaseSource(/* listener= */ this); - } - - @Override - public void onSourceInfoRefreshed( - MediaSource source, Timeline timeline, @Nullable Object manifest) { - refreshSourceInfo(timeline, manifest); - } - @Deprecated private static final class EventListenerWrapper extends DefaultMediaSourceEventListener { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 769f545aaa..ac23e2a831 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -71,7 +71,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); prepareChildSource(/* id= */ null, childSource); } @@ -100,13 +100,12 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) { + protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) { Timeline loopingTimeline = loopCount != Integer.MAX_VALUE ? new LoopingTimeline(timeline, loopCount) : new InfinitelyLoopingTimeline(timeline); - refreshSourceInfo(loopingTimeline, manifest); + refreshSourceInfo(loopingTimeline); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java similarity index 79% rename from library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java index abf02541c8..344c4989eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.source; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; @@ -22,6 +24,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Media period that wraps a media source and defers calling its {@link @@ -29,7 +32,7 @@ import java.io.IOException; * #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media * period immediately but the media source that should create it is not yet prepared. */ -public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { +public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { /** Listener for preparation errors. */ public interface PrepareErrorListener { @@ -42,27 +45,27 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb /** The {@link MediaSource} which will create the actual media period. */ public final MediaSource mediaSource; - /** The {@link MediaPeriodId} used to create the deferred media period. */ + /** The {@link MediaPeriodId} used to create the masking media period. */ public final MediaPeriodId id; private final Allocator allocator; - private MediaPeriod mediaPeriod; - private Callback callback; + @Nullable private MediaPeriod mediaPeriod; + @Nullable private Callback callback; private long preparePositionUs; - private @Nullable PrepareErrorListener listener; + @Nullable private PrepareErrorListener listener; private boolean notifiedPrepareError; private long preparePositionOverrideUs; /** - * Creates a new deferred media period. + * Creates a new masking media period. * * @param mediaSource The media source to wrap. - * @param id The identifier used to create the deferred media period. + * @param id The identifier used to create the masking media period. * @param allocator The allocator used to create the media period. * @param preparePositionUs The expected start position, in microseconds. */ - public DeferredMediaPeriod( + public MaskingMediaPeriod( MediaSource mediaSource, MediaPeriodId id, Allocator allocator, long preparePositionUs) { this.id = id; this.allocator = allocator; @@ -82,7 +85,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb this.listener = listener; } - /** Returns the position at which the deferred media period was prepared, in microseconds. */ + /** Returns the position at which the masking media period was prepared, in microseconds. */ public long getPreparePositionUs() { return preparePositionUs; } @@ -150,53 +153,57 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); + return castNonNull(mediaPeriod).getTrackGroups(); } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { if (preparePositionOverrideUs != C.TIME_UNSET && positionUs == preparePositionUs) { positionUs = preparePositionOverrideUs; preparePositionOverrideUs = C.TIME_UNSET; } - return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs); + return castNonNull(mediaPeriod) + .selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); } @Override public void discardBuffer(long positionUs, boolean toKeyframe) { - mediaPeriod.discardBuffer(positionUs, toKeyframe); + castNonNull(mediaPeriod).discardBuffer(positionUs, toKeyframe); } @Override public long readDiscontinuity() { - return mediaPeriod.readDiscontinuity(); + return castNonNull(mediaPeriod).readDiscontinuity(); } @Override public long getBufferedPositionUs() { - return mediaPeriod.getBufferedPositionUs(); + return castNonNull(mediaPeriod).getBufferedPositionUs(); } @Override public long seekToUs(long positionUs) { - return mediaPeriod.seekToUs(positionUs); + return castNonNull(mediaPeriod).seekToUs(positionUs); } @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return mediaPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters); + return castNonNull(mediaPeriod).getAdjustedSeekPositionUs(positionUs, seekParameters); } @Override public long getNextLoadPositionUs() { - return mediaPeriod.getNextLoadPositionUs(); + return castNonNull(mediaPeriod).getNextLoadPositionUs(); } @Override public void reevaluateBuffer(long positionUs) { - mediaPeriod.reevaluateBuffer(positionUs); + castNonNull(mediaPeriod).reevaluateBuffer(positionUs); } @Override @@ -206,14 +213,14 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); + castNonNull(callback).onContinueLoadingRequested(this); } // MediaPeriod.Callback implementation @Override public void onPrepared(MediaPeriod mediaPeriod) { - callback.onPrepared(this); + castNonNull(callback).onPrepared(this); } private long getPreparePositionWithOverride(long preparePositionUs) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java new file mode 100644 index 0000000000..d9dd83de4f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -0,0 +1,332 @@ +/* + * 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.source; + +import androidx.annotation.Nullable; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A {@link MediaSource} that masks the {@link Timeline} with a placeholder until the actual media + * structure is known. + */ +public final class MaskingMediaSource extends CompositeMediaSource { + + private final MediaSource mediaSource; + private final boolean useLazyPreparation; + private final Timeline.Window window; + private final Timeline.Period period; + + private MaskingTimeline timeline; + @Nullable private MaskingMediaPeriod unpreparedMaskingMediaPeriod; + @Nullable private EventDispatcher unpreparedMaskingMediaPeriodEventDispatcher; + private boolean hasStartedPreparing; + private boolean isPrepared; + + /** + * Creates the masking media source. + * + * @param mediaSource A {@link MediaSource}. + * @param useLazyPreparation Whether the {@code mediaSource} is prepared lazily. If false, all + * manifest loads and other initial preparation steps happen immediately. If true, these + * initial preparations are triggered only when the player starts buffering the media. + */ + public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) { + this.mediaSource = mediaSource; + this.useLazyPreparation = useLazyPreparation; + window = new Timeline.Window(); + period = new Timeline.Period(); + timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag()); + } + + /** Returns the {@link Timeline}. */ + public Timeline getTimeline() { + return timeline; + } + + @Override + public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + if (!useLazyPreparation) { + hasStartedPreparing = true; + prepareChildSource(/* id= */ null, mediaSource); + } + } + + @Nullable + @Override + public Object getTag() { + return mediaSource.getTag(); + } + + @Override + @SuppressWarnings("MissingSuperCall") + public void maybeThrowSourceInfoRefreshError() throws IOException { + // Do nothing. Source info refresh errors will be thrown when calling + // MaskingMediaPeriod.maybeThrowPrepareError. + } + + @Override + public MaskingMediaPeriod createPeriod( + MediaPeriodId id, Allocator allocator, long startPositionUs) { + MaskingMediaPeriod mediaPeriod = + new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs); + if (isPrepared) { + MediaPeriodId idInSource = id.copyWithPeriodUid(getInternalPeriodUid(id.periodUid)); + mediaPeriod.createPeriod(idInSource); + } else { + // We should have at most one media period while source is unprepared because the duration is + // unset and we don't load beyond periods with unset duration. We need to figure out how to + // handle the prepare positions of multiple deferred media periods, should that ever change. + unpreparedMaskingMediaPeriod = mediaPeriod; + unpreparedMaskingMediaPeriodEventDispatcher = + createEventDispatcher(/* windowIndex= */ 0, id, /* mediaTimeOffsetMs= */ 0); + unpreparedMaskingMediaPeriodEventDispatcher.mediaPeriodCreated(); + if (!hasStartedPreparing) { + hasStartedPreparing = true; + prepareChildSource(/* id= */ null, mediaSource); + } + } + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + ((MaskingMediaPeriod) mediaPeriod).releasePeriod(); + if (mediaPeriod == unpreparedMaskingMediaPeriod) { + Assertions.checkNotNull(unpreparedMaskingMediaPeriodEventDispatcher).mediaPeriodReleased(); + unpreparedMaskingMediaPeriodEventDispatcher = null; + unpreparedMaskingMediaPeriod = null; + } + } + + @Override + public void releaseSourceInternal() { + isPrepared = false; + hasStartedPreparing = false; + super.releaseSourceInternal(); + } + + @Override + protected void onChildSourceInfoRefreshed( + Void id, MediaSource mediaSource, Timeline newTimeline) { + if (isPrepared) { + timeline = timeline.cloneWithUpdatedTimeline(newTimeline); + } else if (newTimeline.isEmpty()) { + timeline = + MaskingTimeline.createWithRealTimeline(newTimeline, MaskingTimeline.DUMMY_EXTERNAL_ID); + } else { + // Determine first period and the start position. + // This will be: + // 1. The default window start position if no deferred period has been created yet. + // 2. The non-zero prepare position of the deferred period under the assumption that this is + // a non-zero initial seek position in the window. + // 3. The default window start position if the deferred period has a prepare position of zero + // under the assumption that the prepare position of zero was used because it's the + // default position of the DummyTimeline window. Note that this will override an + // intentional seek to zero for a window with a non-zero default position. This is + // unlikely to be a problem as a non-zero default position usually only occurs for live + // playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions + // anyway. + newTimeline.getWindow(/* windowIndex= */ 0, window); + long windowStartPositionUs = window.getDefaultPositionUs(); + if (unpreparedMaskingMediaPeriod != null) { + long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs(); + if (periodPreparePositionUs != 0) { + windowStartPositionUs = periodPreparePositionUs; + } + } + Pair periodPosition = + newTimeline.getPeriodPosition( + window, period, /* windowIndex= */ 0, windowStartPositionUs); + Object periodUid = periodPosition.first; + long periodPositionUs = periodPosition.second; + timeline = MaskingTimeline.createWithRealTimeline(newTimeline, periodUid); + if (unpreparedMaskingMediaPeriod != null) { + MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; + maskingPeriod.overridePreparePositionUs(periodPositionUs); + MediaPeriodId idInSource = + maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid)); + maskingPeriod.createPeriod(idInSource); + } + } + isPrepared = true; + refreshSourceInfo(this.timeline); + } + + @Nullable + @Override + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Void id, MediaPeriodId mediaPeriodId) { + return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid)); + } + + @Override + protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) { + // Suppress create and release events for the period created while the source was still + // unprepared, as we send these events from this class. + return unpreparedMaskingMediaPeriod == null + || !mediaPeriodId.equals(unpreparedMaskingMediaPeriod.id); + } + + private Object getInternalPeriodUid(Object externalPeriodUid) { + return externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_ID) + ? timeline.replacedInternalId + : externalPeriodUid; + } + + private Object getExternalPeriodUid(Object internalPeriodUid) { + return timeline.replacedInternalId.equals(internalPeriodUid) + ? MaskingTimeline.DUMMY_EXTERNAL_ID + : internalPeriodUid; + } + + /** + * Timeline used as placeholder for an unprepared media source. After preparation, a + * MaskingTimeline is used to keep the originally assigned dummy period ID. + */ + private static final class MaskingTimeline extends ForwardingTimeline { + + public static final Object DUMMY_EXTERNAL_ID = new Object(); + + private final Object replacedInternalId; + + /** + * Returns an instance with a dummy timeline using the provided window tag. + * + * @param windowTag A window tag. + */ + public static MaskingTimeline createWithDummyTimeline(@Nullable Object windowTag) { + return new MaskingTimeline(new DummyTimeline(windowTag), DUMMY_EXTERNAL_ID); + } + + /** + * Returns an instance with a real timeline, replacing the provided period ID with the already + * assigned dummy period ID. + * + * @param timeline The real timeline. + * @param firstPeriodUid The period UID in the timeline which will be replaced by the already + * assigned dummy period UID. + */ + public static MaskingTimeline createWithRealTimeline(Timeline timeline, Object firstPeriodUid) { + return new MaskingTimeline(timeline, firstPeriodUid); + } + + private MaskingTimeline(Timeline timeline, Object replacedInternalId) { + super(timeline); + this.replacedInternalId = replacedInternalId; + } + + /** + * Returns a copy with an updated timeline. This keeps the existing period replacement. + * + * @param timeline The new timeline. + */ + public MaskingTimeline cloneWithUpdatedTimeline(Timeline timeline) { + return new MaskingTimeline(timeline, replacedInternalId); + } + + /** Returns the wrapped timeline. */ + public Timeline getTimeline() { + return timeline; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + timeline.getPeriod(periodIndex, period, setIds); + if (Util.areEqual(period.uid, replacedInternalId)) { + period.uid = DUMMY_EXTERNAL_ID; + } + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline.getIndexOfPeriod(DUMMY_EXTERNAL_ID.equals(uid) ? replacedInternalId : uid); + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + Object uid = timeline.getUidOfPeriod(periodIndex); + return Util.areEqual(uid, replacedInternalId) ? DUMMY_EXTERNAL_ID : uid; + } + } + + /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ + private static final class DummyTimeline extends Timeline { + + @Nullable private final Object tag; + + public DummyTimeline(@Nullable Object tag) { + this.tag = tag; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + return window.set( + tag, + /* manifest= */ null, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ false, + // Dynamic window to indicate pending timeline updates. + /* isDynamic= */ true, + /* defaultPositionUs= */ 0, + /* durationUs= */ C.TIME_UNSET, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ 0, + /* positionInFirstPeriodUs= */ 0); + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return period.set( + /* id= */ 0, + /* uid= */ MaskingTimeline.DUMMY_EXTERNAL_ID, + /* windowIndex= */ 0, + /* durationUs = */ C.TIME_UNSET, + /* positionInWindowUs= */ 0); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return uid == MaskingTimeline.DUMMY_EXTERNAL_ID ? 0 : C.INDEX_UNSET; + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + return MaskingTimeline.DUMMY_EXTERNAL_ID; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index b40bbb35d1..3f306c0c8a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.trackselection.TrackSelection; import java.io.IOException; import java.util.Collections; @@ -57,9 +58,8 @@ public interface MediaPeriod extends SequenceableLoader { * {@link #maybeThrowPrepareError()} will throw an {@link IOException}. * *

If preparation succeeds and results in a source timeline change (e.g. the period duration - * becoming known), {@link - * MediaSource.SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} - * will be called before {@code callback.onPrepared}. + * becoming known), {@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be + * called before {@code callback.onPrepared}. * * @param callback Callback to receive updates from this period, including being notified when * preparation completes. @@ -106,19 +106,24 @@ public interface MediaPeriod extends SequenceableLoader { * Performs a track selection. * *

The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} - * indicating whether the existing {@code SampleStream} can be retained for each selection, and + * indicating whether the existing {@link SampleStream} can be retained for each selection, and * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the * provided selections, clearing, setting and replacing entries as required. If an existing sample * stream is retained but with the requirement that the consuming renderer be reset, then the * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set * if a new sample stream is created. * + *

Note that previously passed {@link TrackSelection TrackSelections} are no longer valid, and + * any references to them must be updated to point to the new selections. + * *

This method is only called after the period has been prepared. * * @param selections The renderer track selections. * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained - * for each selection. A {@code true} value indicates that the selection is unchanged, and - * that the caller does not require that the sample stream be recreated. + * for each track selection. A {@code true} value indicates that the selection is equivalent + * to the one that was previously passed, and that the caller does not require that the sample + * stream be recreated. If a retained sample stream holds any references to the track + * selection then they must be updated to point to the new selection. * @param streams The existing sample streams, which will be updated to reflect the provided * selections. * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index d7b7c75210..5ee980d01f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source; import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; @@ -31,9 +30,9 @@ import java.io.IOException; *

    *
  • To provide the player with a {@link Timeline} defining the structure of its media, and to * provide a new timeline whenever the structure of the media changes. The MediaSource - * provides these timelines by calling {@link SourceInfoRefreshListener#onSourceInfoRefreshed} - * on the {@link SourceInfoRefreshListener}s passed to {@link - * #prepareSource(SourceInfoRefreshListener, TransferListener)}. + * provides these timelines by calling {@link MediaSourceCaller#onSourceInfoRefreshed} on the + * {@link MediaSourceCaller}s passed to {@link #prepareSource(MediaSourceCaller, + * TransferListener)}. *
  • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a * way for the player to load and read the media. @@ -46,25 +45,21 @@ import java.io.IOException; */ public interface MediaSource { - /** Listener for source events. */ - interface SourceInfoRefreshListener { + /** A caller of media sources, which will be notified of source events. */ + interface MediaSourceCaller { /** - * Called when manifest and/or timeline has been refreshed. - *

    - * Called on the playback thread. + * Called when the {@link Timeline} has been refreshed. + * + *

    Called on the playback thread. * * @param source The {@link MediaSource} whose info has been refreshed. * @param timeline The source's timeline. - * @param manifest The loaded manifest. May be null. */ - void onSourceInfoRefreshed(MediaSource source, Timeline timeline, @Nullable Object manifest); - + void onSourceInfoRefreshed(MediaSource source, Timeline timeline); } - /** - * Identifier for a {@link MediaPeriod}. - */ + /** Identifier for a {@link MediaPeriod}. */ final class MediaPeriodId { /** The unique id of the timeline period. */ @@ -240,38 +235,52 @@ public interface MediaSource { } /** - * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest - * updates. + * Registers a {@link MediaSourceCaller}. Starts source preparation if needed and enables the + * source for the creation of {@link MediaPeriod MediaPerods}. * *

    Should not be called directly from application code. * - *

    The listener will be also be notified if the source already has a timeline and/or manifest. + *

    {@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be called once + * the source has a {@link Timeline}. * - *

    For each call to this method, a call to {@link #releaseSource(SourceInfoRefreshListener)} is - * needed to remove the listener and to release the source if no longer required. + *

    For each call to this method, a call to {@link #releaseSource(MediaSourceCaller)} is needed + * to remove the caller and to release the source if no longer required. * - * @param listener The listener to be added. + * @param caller The {@link MediaSourceCaller} to be registered. * @param mediaTransferListener The transfer listener which should be informed of any media data * transfers. May be null if no listener is available. Note that this listener should be only * informed of transfers related to the media loads and not of auxiliary loads for manifests * and other data. */ - void prepareSource( - SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener); + void prepareSource(MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener); /** * Throws any pending error encountered while loading or refreshing source information. - *

    - * Should not be called directly from application code. + * + *

    Should not be called directly from application code. + * + *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. */ void maybeThrowSourceInfoRefreshError() throws IOException; /** - * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called - * multiple times without an intervening call to {@link #releasePeriod(MediaPeriod)}. + * Enables the source for the creation of {@link MediaPeriod MediaPeriods}. * *

    Should not be called directly from application code. * + *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. + * + * @param caller The {@link MediaSourceCaller} enabling the source. + */ + void enable(MediaSourceCaller caller); + + /** + * Returns a new {@link MediaPeriod} identified by {@code periodId}. + * + *

    Should not be called directly from application code. + * + *

    Must only be called if the source is enabled. + * * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param startPositionUs The expected start position, in microseconds. @@ -281,20 +290,36 @@ public interface MediaSource { /** * Releases the period. - *

    - * Should not be called directly from application code. + * + *

    Should not be called directly from application code. * * @param mediaPeriod The period to release. */ void releasePeriod(MediaPeriod mediaPeriod); /** - * Removes a listener for timeline and/or manifest updates and releases the source if no longer - * required. + * Disables the source for the creation of {@link MediaPeriod MediaPeriods}. The implementation + * should not hold onto limited resources used for the creation of media periods. * *

    Should not be called directly from application code. * - * @param listener The listener to be removed. + *

    Must only be called after all {@link MediaPeriod MediaPeriods} previously created by {@link + * #createPeriod(MediaPeriodId, Allocator, long)} have been released by {@link + * #releasePeriod(MediaPeriod)}. + * + * @param caller The {@link MediaSourceCaller} disabling the source. */ - void releaseSource(SourceInfoRefreshListener listener); + void disable(MediaSourceCaller caller); + + /** + * Unregisters a caller, and disables and releases the source if no longer required. + * + *

    Should not be called directly from application code. + * + *

    Must only be called if all created {@link MediaPeriod MediaPeriods} have been released by + * {@link #releasePeriod(MediaPeriod)}. + * + * @param caller The {@link MediaSourceCaller} to be unregistered. + */ + void releaseSource(MediaSourceCaller caller); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 233e19b29c..ab8d86cc55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -101,7 +101,7 @@ public interface MediaSourceEventListener { * The format of the track to which the data belongs. Null if the data does not belong to a * specific track. */ - public final @Nullable Format trackFormat; + @Nullable public final Format trackFormat; /** * One of the {@link C} {@code SELECTION_REASON_*} constants if the data belongs to a track. * {@link C#SELECTION_REASON_UNKNOWN} otherwise. @@ -111,7 +111,7 @@ public interface MediaSourceEventListener { * Optional data associated with the selection of the track to which the data belongs. Null if * the data does not belong to a track. */ - public final @Nullable Object trackSelectionData; + @Nullable public final Object trackSelectionData; /** * The start time of the media, or {@link C#TIME_UNSET} if the data does not belong to a * specific media period. @@ -296,7 +296,7 @@ public interface MediaSourceEventListener { /** The timeline window index reported with the events. */ public final int windowIndex; /** The {@link MediaPeriodId} reported with the events. */ - public final @Nullable MediaPeriodId mediaPeriodId; + @Nullable public final MediaPeriodId mediaPeriodId; private final CopyOnWriteArrayList listenerAndHandlers; private final long mediaTimeOffsetMs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index a4fc8c6b00..cafc052f34 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -23,6 +24,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Merges multiple {@link MediaPeriod}s. @@ -35,9 +37,8 @@ import java.util.IdentityHashMap; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final ArrayList childrenPendingPreparation; - private Callback callback; - private TrackGroupArray trackGroups; - + @Nullable private Callback callback; + @Nullable private TrackGroupArray trackGroups; private MediaPeriod[] enabledPeriods; private SequenceableLoader compositeSequenceableLoader; @@ -49,6 +50,7 @@ import java.util.IdentityHashMap; compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamPeriodIndices = new IdentityHashMap<>(); + enabledPeriods = new MediaPeriod[0]; } @Override @@ -69,12 +71,16 @@ import java.util.IdentityHashMap; @Override public TrackGroupArray getTrackGroups() { - return trackGroups; + return Assertions.checkNotNull(trackGroups); } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { // Map each selection and stream onto a child period index. int[] streamChildIndices = new int[selections.length]; int[] selectionChildIndices = new int[selections.length]; @@ -94,9 +100,9 @@ import java.util.IdentityHashMap; } streamPeriodIndices.clear(); // Select tracks for each child, copying the resulting streams back into a new streams array. - SampleStream[] newStreams = new SampleStream[selections.length]; - SampleStream[] childStreams = new SampleStream[selections.length]; - TrackSelection[] childSelections = new TrackSelection[selections.length]; + @NullableType SampleStream[] newStreams = new SampleStream[selections.length]; + @NullableType SampleStream[] childStreams = new SampleStream[selections.length]; + @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length]; ArrayList enabledPeriodsList = new ArrayList<>(periods.length); for (int i = 0; i < periods.length; i++) { for (int j = 0; j < selections.length; j++) { @@ -114,10 +120,10 @@ import java.util.IdentityHashMap; for (int j = 0; j < selections.length; j++) { if (selectionChildIndices[j] == i) { // Assert that the child provided a stream for the selection. - Assertions.checkState(childStreams[j] != null); + SampleStream childStream = Assertions.checkNotNull(childStreams[j]); newStreams[j] = childStreams[j]; periodEnabled = true; - streamPeriodIndices.put(childStreams[j], i); + streamPeriodIndices.put(childStream, i); } else if (streamChildIndices[j] == i) { // Assert that the child cleared any previous stream. Assertions.checkState(childStreams[j] == null); @@ -208,7 +214,8 @@ import java.util.IdentityHashMap; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return enabledPeriods[0].getAdjustedSeekPositionUs(positionUs, seekParameters); + MediaPeriod queryPeriod = enabledPeriods.length > 0 ? enabledPeriods[0] : periods[0]; + return queryPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters); } // MediaPeriod.Callback implementation @@ -233,12 +240,12 @@ import java.util.IdentityHashMap; } } trackGroups = new TrackGroupArray(trackGroupArray); - callback.onPrepared(this); + Assertions.checkNotNull(callback).onPrepared(this); } @Override public void onContinueLoadingRequested(MediaPeriod ignored) { - callback.onContinueLoadingRequested(this); + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 6b1a362b59..dd7675f3d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -71,9 +71,8 @@ public final class MergingMediaSource extends CompositeMediaSource { private final ArrayList pendingTimelineSources; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private Object primaryManifest; private int periodCount; - private IllegalMergeException mergeError; + @Nullable private IllegalMergeException mergeError; /** * @param mediaSources The {@link MediaSource}s to merge. @@ -104,7 +103,7 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); for (int i = 0; i < mediaSources.length; i++) { prepareChildSource(i, mediaSources[i]); @@ -140,10 +139,9 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); Arrays.fill(timelines, null); - primaryManifest = null; periodCount = PERIOD_COUNT_UNSET; mergeError = null; pendingTimelineSources.clear(); @@ -152,7 +150,7 @@ public final class MergingMediaSource extends CompositeMediaSource { @Override protected void onChildSourceInfoRefreshed( - Integer id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) { + Integer id, MediaSource mediaSource, Timeline timeline) { if (mergeError == null) { mergeError = checkTimelineMerges(timeline); } @@ -161,20 +159,19 @@ public final class MergingMediaSource extends CompositeMediaSource { } pendingTimelineSources.remove(mediaSource); timelines[id] = timeline; - if (mediaSource == mediaSources[0]) { - primaryManifest = manifest; - } if (pendingTimelineSources.isEmpty()) { - refreshSourceInfo(timelines[0], primaryManifest); + refreshSourceInfo(timelines[0]); } } @Override - protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + @Nullable + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( Integer id, MediaPeriodId mediaPeriodId) { return id == 0 ? mediaPeriodId : null; } + @Nullable private IllegalMergeException checkTimelineMerges(Timeline timeline) { if (periodCount == PERIOD_COUNT_UNSET) { periodCount = timeline.getPeriodCount(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index d9f0008a7f..d25fff5104 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -90,6 +91,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final Uri uri; private final DataSource dataSource; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Listener listener; @@ -107,6 +109,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Nullable private SeekMap seekMap; @Nullable private IcyHeaders icyHeaders; private SampleQueue[] sampleQueues; + private DecryptableSampleQueueReader[] sampleQueueReaders; private TrackId[] sampleQueueTrackIds; private boolean sampleQueuesBuilt; private boolean prepared; @@ -152,6 +155,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Uri uri, DataSource dataSource, Extractor[] extractors, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, Listener listener, @@ -160,6 +164,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSource = dataSource; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.listener = listener; @@ -180,6 +185,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; handler = new Handler(); sampleQueueTrackIds = new TrackId[0]; sampleQueues = new SampleQueue[0]; + sampleQueueReaders = new DecryptableSampleQueueReader[0]; pendingResetPositionUs = C.TIME_UNSET; length = C.LENGTH_UNSET; durationUs = C.TIME_UNSET; @@ -194,6 +200,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.discardToEnd(); } + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } } loader.release(/* callback= */ this); handler.removeCallbacksAndMessages(null); @@ -207,6 +216,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.reset(); } + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } extractorHolder.release(); } @@ -432,24 +444,32 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // SampleStream methods. /* package */ boolean isReady(int track) { - return !suppressRead() && (loadingFinished || sampleQueues[track].hasNextSample()); + return !suppressRead() && sampleQueueReaders[track].isReady(loadingFinished); + } + + /* package */ void maybeThrowError(int sampleQueueIndex) throws IOException { + sampleQueueReaders[sampleQueueIndex].maybeThrowError(); + maybeThrowError(); } /* package */ void maybeThrowError() throws IOException { loader.maybeThrowError(loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType)); } - /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, + /* package */ int readData( + int sampleQueueIndex, + FormatHolder formatHolder, + DecoderInputBuffer buffer, boolean formatRequired) { if (suppressRead()) { return C.RESULT_NOTHING_READ; } - maybeNotifyDownstreamFormat(track); + maybeNotifyDownstreamFormat(sampleQueueIndex); int result = - sampleQueues[track].read( + sampleQueueReaders[sampleQueueIndex].read( formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); if (result == C.RESULT_NOTHING_READ) { - maybeStartDeferredRetry(track); + maybeStartDeferredRetry(sampleQueueIndex); } return result; } @@ -662,6 +682,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @NullableType SampleQueue[] sampleQueues = Arrays.copyOf(this.sampleQueues, trackCount + 1); sampleQueues[trackCount] = trackOutput; this.sampleQueues = Util.castNonNullTypeArray(sampleQueues); + @NullableType + DecryptableSampleQueueReader[] sampleQueueReaders = + Arrays.copyOf(this.sampleQueueReaders, trackCount + 1); + sampleQueueReaders[trackCount] = + new DecryptableSampleQueueReader(this.sampleQueues[trackCount], drmSessionManager); + this.sampleQueueReaders = Util.castNonNullTypeArray(sampleQueueReaders); return trackOutput; } @@ -733,7 +759,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (prepared) { SeekMap seekMap = getPreparedState().seekMap; Assertions.checkState(isPendingReset()); - if (durationUs != C.TIME_UNSET && pendingResetPositionUs >= durationUs) { + if (durationUs != C.TIME_UNSET && pendingResetPositionUs > durationUs) { loadingFinished = true; pendingResetPositionUs = C.TIME_UNSET; return; @@ -863,7 +889,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void maybeThrowError() throws IOException { - ProgressiveMediaPeriod.this.maybeThrowError(); + ProgressiveMediaPeriod.this.maybeThrowError(track); } @Override @@ -1013,7 +1039,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final Extractor[] extractors; - private @Nullable Extractor extractor; + @Nullable private Extractor extractor; /** * Creates a holder that will select an extractor and initialize it using the specified output. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index 5ed12154b3..bd32587bdd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; @@ -51,6 +53,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource private ExtractorsFactory extractorsFactory; @Nullable private String customCacheKey; @Nullable private Object tag; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; private boolean isCreateCalled; @@ -74,6 +77,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) { this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; } @@ -128,6 +132,20 @@ public final class ProgressiveMediaSource extends BaseMediaSource return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + /** * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. @@ -172,6 +190,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource uri, dataSourceFactory, extractorsFactory, + drmSessionManager, loadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, @@ -193,6 +212,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource private final Uri uri; private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; @Nullable private final String customCacheKey; private final int continueLoadingCheckIntervalBytes; @@ -207,6 +227,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, @Nullable String customCacheKey, int continueLoadingCheckIntervalBytes, @@ -214,6 +235,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; + this.drmSessionManager = drmSessionManager; this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy; this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; @@ -228,7 +250,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); } @@ -248,6 +270,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource uri, dataSource, extractorsFactory.createExtractors(), + drmSessionManager, loadableLoadErrorHandlingPolicy, createEventDispatcher(id), this, @@ -262,7 +285,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { // Do nothing. } @@ -287,7 +310,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. refreshSourceInfo( new SinglePeriodTimeline( - timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, tag), - /* manifest= */ null); + timelineDurationUs, + timelineIsSeekable, + /* isDynamic= */ false, + /* manifest= */ null, + tag)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 25cc73d4ae..09bc438f90 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; +import com.google.android.exoplayer2.source.SampleQueue.PeekResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -61,6 +62,7 @@ import com.google.android.exoplayer2.util.Util; private boolean upstreamKeyframeRequired; private boolean upstreamFormatRequired; private Format upstreamFormat; + private Format upstreamCommittedFormat; private int upstreamSourceId; public SampleMetadataQueue() { @@ -95,6 +97,7 @@ import com.google.android.exoplayer2.util.Util; largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; isLastSampleQueued = false; + upstreamCommittedFormat = null; if (resetUpstreamFormat) { upstreamFormat = null; upstreamFormatRequired = true; @@ -214,6 +217,27 @@ import com.google.android.exoplayer2.util.Util; readPosition = 0; } + /** + * Returns a {@link PeekResult} depending on what a following call to {@link #read + * read(formatHolder, decoderInputBuffer, formatRequired= false, allowOnlyClearBuffers= false, + * loadingFinished= false, decodeOnlyUntilUs= 0)} would result in. + */ + @SuppressWarnings("ReferenceEquality") + @PeekResult + public synchronized int peekNext(Format downstreamFormat) { + if (readPosition == length) { + return SampleQueue.PEEK_RESULT_NOTHING; + } + int relativeReadIndex = getRelativeIndex(readPosition); + if (formats[relativeReadIndex] != downstreamFormat) { + return SampleQueue.PEEK_RESULT_FORMAT; + } else { + return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0 + ? SampleQueue.PEEK_RESULT_BUFFER_ENCRYPTED + : SampleQueue.PEEK_RESULT_BUFFER_CLEAR; + } + } + /** * Attempts to read from the queue. * @@ -230,6 +254,8 @@ import com.google.android.exoplayer2.util.Util; * @param formatRequired Whether the caller requires that the format of the stream be read even if * it's not changing. A sample will never be read if set to true, however it is still possible * for the end of stream or nothing to be read. + * @param allowOnlyClearBuffers If set to true, this method will not return encrypted buffers, + * returning {@link C#RESULT_NOTHING_READ} (without advancing the read position) instead. * @param loadingFinished True if an empty queue should be considered the end of the stream. * @param downstreamFormat The current downstream {@link Format}. If the format of the next sample * is different to the current downstream format then a format will be read. @@ -242,6 +268,7 @@ import com.google.android.exoplayer2.util.Util; FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, + boolean allowOnlyClearBuffers, boolean loadingFinished, Format downstreamFormat, SampleExtrasHolder extrasHolder) { @@ -249,8 +276,7 @@ import com.google.android.exoplayer2.util.Util; if (loadingFinished || isLastSampleQueued) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; - } else if (upstreamFormat != null - && (formatRequired || upstreamFormat != downstreamFormat)) { + } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) { formatHolder.format = upstreamFormat; return C.RESULT_FORMAT_READ; } else { @@ -264,6 +290,10 @@ import com.google.android.exoplayer2.util.Util; return C.RESULT_FORMAT_READ; } + if (allowOnlyClearBuffers && (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0) { + return C.RESULT_NOTHING_READ; + } + buffer.setFlags(flags[relativeReadIndex]); buffer.timeUs = timesUs[relativeReadIndex]; if (buffer.isFlagsOnly()) { @@ -393,8 +423,16 @@ import com.google.android.exoplayer2.util.Util; } upstreamFormatRequired = false; if (Util.areEqual(format, upstreamFormat)) { - // Suppress changes between equal formats so we can use referential equality in readData. + // The format is unchanged. If format and upstreamFormat are different objects, we keep the + // current upstreamFormat so we can detect format changes in read() using cheap referential + // equality. return false; + } else if (Util.areEqual(format, upstreamCommittedFormat)) { + // The format has changed back to the format of the last committed sample. If they are + // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat so + // we can detect format changes in read() using cheap referential equality. + upstreamFormat = upstreamCommittedFormat; + return true; } else { upstreamFormat = format; return true; @@ -422,6 +460,7 @@ import com.google.android.exoplayer2.util.Util; cryptoDatas[relativeEndIndex] = cryptoData; formats[relativeEndIndex] = upstreamFormat; sourceIds[relativeEndIndex] = upstreamSourceId; + upstreamCommittedFormat = upstreamFormat; length++; if (length == capacity) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index e8f4953436..921afcdf2f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -28,6 +29,9 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; /** A queue of media samples. */ @@ -47,6 +51,27 @@ public class SampleQueue implements TrackOutput { } + /** Values returned by {@link #peekNext()}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + value = { + PEEK_RESULT_NOTHING, + PEEK_RESULT_FORMAT, + PEEK_RESULT_BUFFER_CLEAR, + PEEK_RESULT_BUFFER_ENCRYPTED + }) + @interface PeekResult {} + + /** Nothing is available for reading. */ + public static final int PEEK_RESULT_NOTHING = 0; + /** A format change is available for reading */ + public static final int PEEK_RESULT_FORMAT = 1; + /** A clear buffer is available for reading. */ + public static final int PEEK_RESULT_BUFFER_CLEAR = 2; + /** An encrypted buffer is available for reading. */ + public static final int PEEK_RESULT_BUFFER_ENCRYPTED = 3; + public static final int ADVANCE_FAILED = -1; private static final int INITIAL_SCRATCH_SIZE = 32; @@ -312,6 +337,16 @@ public class SampleQueue implements TrackOutput { return metadataQueue.setReadPosition(sampleIndex); } + /** + * Returns a {@link PeekResult} depending on what a following call to {@link #read + * read(formatHolder, decoderInputBuffer, formatRequired= false, allowOnlyClearBuffers= false, + * loadingFinished= false, decodeOnlyUntilUs= 0)} would result in. + */ + @PeekResult + public int peekNext() { + return metadataQueue.peekNext(downstreamFormat); + } + /** * Attempts to read from the queue. * @@ -324,6 +359,8 @@ public class SampleQueue implements TrackOutput { * @param formatRequired Whether the caller requires that the format of the stream be read even if * it's not changing. A sample will never be read if set to true, however it is still possible * for the end of stream or nothing to be read. + * @param allowOnlyClearBuffers If set to true, this method will not return encrypted buffers, + * returning {@link C#RESULT_NOTHING_READ} (without advancing the read position) instead. * @param loadingFinished True if an empty queue should be considered the end of the stream. * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will * be set if the buffer's timestamp is less than this value. @@ -334,10 +371,18 @@ public class SampleQueue implements TrackOutput { FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, + boolean allowOnlyClearBuffers, boolean loadingFinished, long decodeOnlyUntilUs) { - int result = metadataQueue.read(formatHolder, buffer, formatRequired, loadingFinished, - downstreamFormat, extrasHolder); + int result = + metadataQueue.read( + formatHolder, + buffer, + formatRequired, + allowOnlyClearBuffers, + loadingFinished, + downstreamFormat, + extrasHolder); switch (result) { case C.RESULT_FORMAT_READ: downstreamFormat = formatHolder.format; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java new file mode 100644 index 0000000000..c3eab68983 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -0,0 +1,247 @@ +/* + * 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.source; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** Media source with a single period consisting of silent raw audio of a given duration. */ +public final class SilenceMediaSource extends BaseMediaSource { + + private static final int SAMPLE_RATE_HZ = 44100; + @C.PcmEncoding private static final int ENCODING = C.ENCODING_PCM_16BIT; + private static final int CHANNEL_COUNT = 2; + private static final Format FORMAT = + Format.createAudioSampleFormat( + /* id=*/ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + CHANNEL_COUNT, + SAMPLE_RATE_HZ, + ENCODING, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + private static final byte[] SILENCE_SAMPLE = + new byte[Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * 1024]; + + private final long durationUs; + + /** + * Creates a new media source providing silent audio of the given duration. + * + * @param durationUs The duration of silent audio to output, in microseconds. + */ + public SilenceMediaSource(long durationUs) { + Assertions.checkArgument(durationUs >= 0); + this.durationUs = durationUs; + } + + @Override + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + refreshSourceInfo( + new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false)); + } + + @Override + public void maybeThrowSourceInfoRefreshError() {} + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + return new SilenceMediaPeriod(durationUs); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) {} + + @Override + protected void releaseSourceInternal() {} + + private static final class SilenceMediaPeriod implements MediaPeriod { + + private static final TrackGroupArray TRACKS = new TrackGroupArray(new TrackGroup(FORMAT)); + + private final long durationUs; + private final ArrayList sampleStreams; + + public SilenceMediaPeriod(long durationUs) { + this.durationUs = durationUs; + sampleStreams = new ArrayList<>(); + } + + @Override + public void prepare(Callback callback, long positionUs) { + callback.onPrepared(/* mediaPeriod= */ this); + } + + @Override + public void maybeThrowPrepareError() {} + + @Override + public TrackGroupArray getTrackGroups() { + return TRACKS; + } + + @Override + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { + positionUs = constrainSeekPosition(positionUs); + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + sampleStreams.remove(streams[i]); + streams[i] = null; + } + if (streams[i] == null && selections[i] != null) { + SilenceSampleStream stream = new SilenceSampleStream(durationUs); + stream.seekTo(positionUs); + sampleStreams.add(stream); + streams[i] = stream; + streamResetFlags[i] = true; + } + } + return positionUs; + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) {} + + @Override + public long readDiscontinuity() { + return C.TIME_UNSET; + } + + @Override + public long seekToUs(long positionUs) { + positionUs = constrainSeekPosition(positionUs); + for (int i = 0; i < sampleStreams.size(); i++) { + ((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs); + } + return positionUs; + } + + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return constrainSeekPosition(positionUs); + } + + @Override + public long getBufferedPositionUs() { + return C.TIME_END_OF_SOURCE; + } + + @Override + public long getNextLoadPositionUs() { + return C.TIME_END_OF_SOURCE; + } + + @Override + public boolean continueLoading(long positionUs) { + return false; + } + + @Override + public void reevaluateBuffer(long positionUs) {} + + private long constrainSeekPosition(long positionUs) { + return Util.constrainValue(positionUs, 0, durationUs); + } + } + + private static final class SilenceSampleStream implements SampleStream { + + private final long durationBytes; + + private boolean sentFormat; + private long positionBytes; + + public SilenceSampleStream(long durationUs) { + durationBytes = getAudioByteCount(durationUs); + seekTo(0); + } + + public void seekTo(long positionUs) { + positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void maybeThrowError() {} + + @Override + public int readData( + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { + if (!sentFormat || formatRequired) { + formatHolder.format = FORMAT; + sentFormat = true; + return C.RESULT_FORMAT_READ; + } + + long bytesRemaining = durationBytes - positionBytes; + if (bytesRemaining == 0) { + buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } + + int bytesToWrite = (int) Math.min(SILENCE_SAMPLE.length, bytesRemaining); + buffer.ensureSpaceForWrite(bytesToWrite); + buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); + buffer.data.put(SILENCE_SAMPLE, /* offset= */ 0, bytesToWrite); + buffer.timeUs = getAudioPositionUs(positionBytes); + positionBytes += bytesToWrite; + return C.RESULT_BUFFER_READ; + } + + @Override + public int skipData(long positionUs) { + long oldPositionBytes = positionBytes; + seekTo(positionUs); + return (int) ((positionBytes - oldPositionBytes) / SILENCE_SAMPLE.length); + } + } + + private static long getAudioByteCount(long durationUs) { + long audioSampleCount = durationUs * SAMPLE_RATE_HZ / C.MICROS_PER_SECOND; + return Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * audioSampleCount; + } + + private static long getAudioPositionUs(long bytes) { + long audioSampleCount = bytes / Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT); + return audioSampleCount * C.MICROS_PER_SECOND / SAMPLE_RATE_HZ; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index acdfbcc8c0..8790b09f07 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -35,7 +35,8 @@ public final class SinglePeriodTimeline extends Timeline { private final long windowDefaultStartPositionUs; private final boolean isSeekable; private final boolean isDynamic; - private final @Nullable Object tag; + @Nullable private final Object tag; + @Nullable private final Object manifest; /** * Creates a timeline containing a single period and a window that spans it. @@ -45,7 +46,7 @@ public final class SinglePeriodTimeline extends Timeline { * @param isDynamic Whether the window may change when the timeline is updated. */ public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { - this(durationUs, isSeekable, isDynamic, /* tag= */ null); + this(durationUs, isSeekable, isDynamic, /* manifest= */ null, /* tag= */ null); } /** @@ -54,10 +55,15 @@ public final class SinglePeriodTimeline extends Timeline { * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. * @param isDynamic Whether the window may change when the timeline is updated. - * @param tag A tag used for {@link Timeline.Window#tag}. + * @param manifest The manifest. May be {@code null}. + * @param tag A tag used for {@link Window#tag}. */ public SinglePeriodTimeline( - long durationUs, boolean isSeekable, boolean isDynamic, @Nullable Object tag) { + long durationUs, + boolean isSeekable, + boolean isDynamic, + @Nullable Object manifest, + @Nullable Object tag) { this( durationUs, durationUs, @@ -65,6 +71,7 @@ public final class SinglePeriodTimeline extends Timeline { /* windowDefaultStartPositionUs= */ 0, isSeekable, isDynamic, + manifest, tag); } @@ -80,6 +87,7 @@ public final class SinglePeriodTimeline extends Timeline { * which to begin playback, in microseconds. * @param isSeekable Whether seeking is supported within the window. * @param isDynamic Whether the window may change when the timeline is updated. + * @param manifest The manifest. May be (@code null}. * @param tag A tag used for {@link Timeline.Window#tag}. */ public SinglePeriodTimeline( @@ -89,6 +97,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + @Nullable Object manifest, @Nullable Object tag) { this( /* presentationStartTimeMs= */ C.TIME_UNSET, @@ -99,6 +108,7 @@ public final class SinglePeriodTimeline extends Timeline { windowDefaultStartPositionUs, isSeekable, isDynamic, + manifest, tag); } @@ -117,6 +127,7 @@ public final class SinglePeriodTimeline extends Timeline { * which to begin playback, in microseconds. * @param isSeekable Whether seeking is supported within the window. * @param isDynamic Whether the window may change when the timeline is updated. + * @param manifest The manifest. May be {@code null}. * @param tag A tag used for {@link Timeline.Window#tag}. */ public SinglePeriodTimeline( @@ -128,6 +139,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + @Nullable Object manifest, @Nullable Object tag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; @@ -137,6 +149,7 @@ public final class SinglePeriodTimeline extends Timeline { this.windowDefaultStartPositionUs = windowDefaultStartPositionUs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.manifest = manifest; this.tag = tag; } @@ -165,6 +178,7 @@ public final class SinglePeriodTimeline extends Timeline { } return window.set( tag, + manifest, presentationStartTimeMs, windowStartTimeMs, isSeekable, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index e0c2a00df3..62d873868e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -31,11 +31,14 @@ import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.upstream.Loader.Loadable; import com.google.android.exoplayer2.upstream.StatsDataSource; import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link MediaPeriod} with a single sample. @@ -50,7 +53,7 @@ import java.util.Arrays; private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final TrackGroupArray tracks; @@ -64,8 +67,7 @@ import java.util.Arrays; /* package */ boolean notifiedReadingStarted; /* package */ boolean loadingFinished; - /* package */ boolean loadingSucceeded; - /* package */ byte[] sampleData; + /* package */ byte @MonotonicNonNull [] sampleData; /* package */ int sampleSize; public SingleSampleMediaPeriod( @@ -112,8 +114,12 @@ import java.util.Arrays; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { sampleStreams.remove(streams[i]); @@ -204,9 +210,8 @@ import java.util.Arrays; public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { sampleSize = (int) loadable.dataSource.getBytesRead(); - sampleData = loadable.sampleData; + sampleData = Assertions.checkNotNull(loadable.sampleData); loadingFinished = true; - loadingSucceeded = true; eventDispatcher.loadCompleted( loadable.dataSpec, loadable.dataSource.getLastOpenedUri(), @@ -325,7 +330,7 @@ import java.util.Arrays; streamState = STREAM_STATE_SEND_SAMPLE; return C.RESULT_FORMAT_READ; } else if (loadingFinished) { - if (loadingSucceeded) { + if (sampleData != null) { buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); buffer.timeUs = 0; if (buffer.isFlagsOnly()) { @@ -371,7 +376,7 @@ import java.util.Arrays; private final StatsDataSource dataSource; - private byte[] sampleData; + @Nullable private byte[] sampleData; public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { this.dataSpec = dataSpec; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 6f85a2b0f8..04ee3a153c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -60,7 +60,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean treatLoadErrorsAsEndOfStream; private boolean isCreateCalled; - private @Nullable Object tag; + @Nullable private Object tag; /** * Creates a factory for {@link SingleSampleMediaSource}s. @@ -186,7 +186,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { private final Timeline timeline; @Nullable private final Object tag; - private @Nullable TransferListener transferListener; + @Nullable private TransferListener transferListener; /** * @param uri The {@link Uri} of the media stream. @@ -290,7 +290,8 @@ public final class SingleSampleMediaSource extends BaseMediaSource { this.tag = tag; dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); timeline = - new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false, tag); + new SinglePeriodTimeline( + durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* manifest= */ null, tag); } // MediaSource implementation. @@ -302,9 +303,9 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; - refreshSourceInfo(timeline, /* manifest= */ null); + refreshSourceInfo(timeline); } @Override @@ -331,7 +332,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index be9dea91f1..0a1628b3f9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -18,12 +18,15 @@ package com.google.android.exoplayer2.source.ads; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.IntDef; +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.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Represents ad group times relative to the start of the media and information on the state and @@ -45,9 +48,9 @@ public final class AdPlaybackState { /** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */ public final int count; /** The URI of each ad in the ad group. */ - public final Uri[] uris; + public final @NullableType Uri[] uris; /** The state of each ad in the ad group. */ - public final @AdState int[] states; + @AdState public final int[] states; /** The durations of each ad in the ad group, in microseconds. */ public final long[] durationsUs; @@ -60,7 +63,8 @@ public final class AdPlaybackState { /* durationsUs= */ new long[0]); } - private AdGroup(int count, @AdState int[] states, Uri[] uris, long[] durationsUs) { + private AdGroup( + int count, @AdState int[] states, @NullableType Uri[] uris, long[] durationsUs) { Assertions.checkArgument(states.length == uris.length); this.count = count; this.states = states; @@ -98,7 +102,7 @@ public final class AdPlaybackState { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -130,7 +134,7 @@ public final class AdPlaybackState { Assertions.checkArgument(this.count == C.LENGTH_UNSET && states.length <= count); @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count); long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count); - Uri[] uris = Arrays.copyOf(this.uris, count); + @NullableType Uri[] uris = Arrays.copyOf(this.uris, count); return new AdGroup(count, states, uris, durationsUs); } @@ -151,7 +155,7 @@ public final class AdPlaybackState { this.durationsUs.length == states.length ? this.durationsUs : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length); - Uri[] uris = Arrays.copyOf(this.uris, states.length); + @NullableType Uri[] uris = Arrays.copyOf(this.uris, states.length); uris[index] = uri; states[index] = AD_STATE_AVAILABLE; return new AdGroup(count, states, uris, durationsUs); @@ -177,6 +181,7 @@ public final class AdPlaybackState { this.durationsUs.length == states.length ? this.durationsUs : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length); + @NullableType Uri[] uris = this.uris.length == states.length ? this.uris : Arrays.copyOf(this.uris, states.length); states[index] = state; @@ -362,7 +367,7 @@ public final class AdPlaybackState { if (adGroups[adGroupIndex].count == adCount) { return this; } - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = this.adGroups[adGroupIndex].withAdCount(adCount); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -370,7 +375,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad URI. */ @CheckResult public AdPlaybackState withAdUri(int adGroupIndex, int adIndexInAdGroup, Uri uri) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdUri(uri, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -378,7 +383,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad marked as played. */ @CheckResult public AdPlaybackState withPlayedAd(int adGroupIndex, int adIndexInAdGroup) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_PLAYED, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -386,7 +391,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad marked as skipped. */ @CheckResult public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -394,7 +399,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad marked as having a load error. */ @CheckResult public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_ERROR, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -405,7 +410,7 @@ public final class AdPlaybackState { */ @CheckResult public AdPlaybackState withSkippedAdGroup(int adGroupIndex) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAllAdsSkipped(); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -413,7 +418,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad durations, in microseconds. */ @CheckResult public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) { adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]); } @@ -441,7 +446,7 @@ public final class AdPlaybackState { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 8828e34304..5e22de4320 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -23,7 +23,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.CompositeMediaSource; -import com.google.android.exoplayer2.source.DeferredMediaPeriod; +import com.google.android.exoplayer2.source.MaskingMediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -47,6 +47,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A {@link MediaSource} that inserts ads linearly with a provided content media source. This source @@ -114,7 +115,7 @@ public final class AdsMediaSource extends CompositeMediaSource { */ public RuntimeException getRuntimeExceptionForUnexpected() { Assertions.checkState(type == TYPE_UNEXPECTED); - return (RuntimeException) getCause(); + return (RuntimeException) Assertions.checkNotNull(getCause()); } } @@ -127,16 +128,15 @@ public final class AdsMediaSource extends CompositeMediaSource { private final AdsLoader adsLoader; private final AdsLoader.AdViewProvider adViewProvider; private final Handler mainHandler; - private final Map> deferredMediaPeriodByAdMediaSource; + private final Map> maskingMediaPeriodByAdMediaSource; private final Timeline.Period period; // Accessed on the player thread. - private ComponentListener componentListener; - private Timeline contentTimeline; - private Object contentManifest; - private AdPlaybackState adPlaybackState; - private MediaSource[][] adGroupMediaSources; - private Timeline[][] adGroupTimelines; + @Nullable private ComponentListener componentListener; + @Nullable private Timeline contentTimeline; + @Nullable private AdPlaybackState adPlaybackState; + private @NullableType MediaSource[][] adGroupMediaSources; + private @NullableType Timeline[][] adGroupTimelines; /** * Constructs a new source that inserts ads linearly with the content specified by {@code @@ -178,7 +178,7 @@ public final class AdsMediaSource extends CompositeMediaSource { this.adsLoader = adsLoader; this.adViewProvider = adViewProvider; mainHandler = new Handler(Looper.getMainLooper()); - deferredMediaPeriodByAdMediaSource = new HashMap<>(); + maskingMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; adGroupTimelines = new Timeline[0][]; @@ -192,7 +192,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); ComponentListener componentListener = new ComponentListener(); this.componentListener = componentListener; @@ -202,43 +202,45 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + AdPlaybackState adPlaybackState = Assertions.checkNotNull(this.adPlaybackState); if (adPlaybackState.adGroupCount > 0 && id.isAd()) { int adGroupIndex = id.adGroupIndex; int adIndexInAdGroup = id.adIndexInAdGroup; - Uri adUri = adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]; + Uri adUri = + Assertions.checkNotNull(adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]); if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adUri); - int oldAdCount = adGroupMediaSources[adGroupIndex].length; - if (adIndexInAdGroup >= oldAdCount) { - int adCount = adIndexInAdGroup + 1; - adGroupMediaSources[adGroupIndex] = - Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); - adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount); - } - adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; - deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList<>()); - prepareChildSource(id, adMediaSource); + int adCount = adIndexInAdGroup + 1; + adGroupMediaSources[adGroupIndex] = + Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); + adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount); } MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - DeferredMediaPeriod deferredMediaPeriod = - new DeferredMediaPeriod(mediaSource, id, allocator, startPositionUs); - deferredMediaPeriod.setPrepareErrorListener( + if (mediaSource == null) { + mediaSource = adMediaSourceFactory.createMediaSource(adUri); + adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = mediaSource; + maskingMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>()); + prepareChildSource(id, mediaSource); + } + MaskingMediaPeriod maskingMediaPeriod = + new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs); + maskingMediaPeriod.setPrepareErrorListener( new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); - List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + List mediaPeriods = maskingMediaPeriodByAdMediaSource.get(mediaSource); if (mediaPeriods == null) { Object periodUid = - adGroupTimelines[adGroupIndex][adIndexInAdGroup].getUidOfPeriod(/* periodIndex= */ 0); + Assertions.checkNotNull(adGroupTimelines[adGroupIndex][adIndexInAdGroup]) + .getUidOfPeriod(/* periodIndex= */ 0); MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber); - deferredMediaPeriod.createPeriod(adSourceMediaPeriodId); + maskingMediaPeriod.createPeriod(adSourceMediaPeriodId); } else { - // Keep track of the deferred media period so it can be populated with the real media period + // Keep track of the masking media period so it can be populated with the real media period // when the source's info becomes available. - mediaPeriods.add(deferredMediaPeriod); + mediaPeriods.add(maskingMediaPeriod); } - return deferredMediaPeriod; + return maskingMediaPeriod; } else { - DeferredMediaPeriod mediaPeriod = - new DeferredMediaPeriod(contentMediaSource, id, allocator, startPositionUs); + MaskingMediaPeriod mediaPeriod = + new MaskingMediaPeriod(contentMediaSource, id, allocator, startPositionUs); mediaPeriod.createPeriod(id); return mediaPeriod; } @@ -246,23 +248,22 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override public void releasePeriod(MediaPeriod mediaPeriod) { - DeferredMediaPeriod deferredMediaPeriod = (DeferredMediaPeriod) mediaPeriod; - List mediaPeriods = - deferredMediaPeriodByAdMediaSource.get(deferredMediaPeriod.mediaSource); + MaskingMediaPeriod maskingMediaPeriod = (MaskingMediaPeriod) mediaPeriod; + List mediaPeriods = + maskingMediaPeriodByAdMediaSource.get(maskingMediaPeriod.mediaSource); if (mediaPeriods != null) { - mediaPeriods.remove(deferredMediaPeriod); + mediaPeriods.remove(maskingMediaPeriod); } - deferredMediaPeriod.releasePeriod(); + maskingMediaPeriod.releasePeriod(); } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); - componentListener.release(); + Assertions.checkNotNull(componentListener).release(); componentListener = null; - deferredMediaPeriodByAdMediaSource.clear(); + maskingMediaPeriodByAdMediaSource.clear(); contentTimeline = null; - contentManifest = null; adPlaybackState = null; adGroupMediaSources = new MediaSource[0][]; adGroupTimelines = new Timeline[0][]; @@ -271,16 +272,13 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override protected void onChildSourceInfoRefreshed( - MediaPeriodId mediaPeriodId, - MediaSource mediaSource, - Timeline timeline, - @Nullable Object manifest) { + MediaPeriodId mediaPeriodId, MediaSource mediaSource, Timeline timeline) { if (mediaPeriodId.isAd()) { int adGroupIndex = mediaPeriodId.adGroupIndex; int adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup; onAdSourceInfoRefreshed(mediaSource, adGroupIndex, adIndexInAdGroup, timeline); } else { - onContentSourceInfoRefreshed(timeline, manifest); + onContentSourceInfoRefreshed(timeline); } } @@ -305,10 +303,9 @@ public final class AdsMediaSource extends CompositeMediaSource { maybeUpdateSourceInfo(); } - private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) { + private void onContentSourceInfoRefreshed(Timeline timeline) { Assertions.checkArgument(timeline.getPeriodCount() == 1); contentTimeline = timeline; - contentManifest = manifest; maybeUpdateSourceInfo(); } @@ -316,11 +313,11 @@ public final class AdsMediaSource extends CompositeMediaSource { int adIndexInAdGroup, Timeline timeline) { Assertions.checkArgument(timeline.getPeriodCount() == 1); adGroupTimelines[adGroupIndex][adIndexInAdGroup] = timeline; - List mediaPeriods = deferredMediaPeriodByAdMediaSource.remove(mediaSource); + List mediaPeriods = maskingMediaPeriodByAdMediaSource.remove(mediaSource); if (mediaPeriods != null) { Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); for (int i = 0; i < mediaPeriods.size(); i++) { - DeferredMediaPeriod mediaPeriod = mediaPeriods.get(i); + MaskingMediaPeriod mediaPeriod = mediaPeriods.get(i); MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber); mediaPeriod.createPeriod(adSourceMediaPeriodId); @@ -330,17 +327,19 @@ public final class AdsMediaSource extends CompositeMediaSource { } private void maybeUpdateSourceInfo() { + Timeline contentTimeline = this.contentTimeline; if (adPlaybackState != null && contentTimeline != null) { adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurations(adGroupTimelines, period)); Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState); - refreshSourceInfo(timeline, contentManifest); + refreshSourceInfo(timeline); } } - private static long[][] getAdDurations(Timeline[][] adTimelines, Timeline.Period period) { + private static long[][] getAdDurations( + @NullableType Timeline[][] adTimelines, Timeline.Period period) { long[][] adDurations = new long[adTimelines.length][]; for (int i = 0; i < adTimelines.length; i++) { adDurations[i] = new long[adTimelines[i].length]; @@ -408,7 +407,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } } - private final class AdPrepareErrorListener implements DeferredMediaPeriod.PrepareErrorListener { + private final class AdPrepareErrorListener implements MaskingMediaPeriod.PrepareErrorListener { private final Uri adUri; private final int adGroupIndex; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java index 68322c60a1..74d8ddad3d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.upstream.DataSource; @@ -58,7 +59,7 @@ public abstract class BaseMediaChunk extends MediaChunk { DataSpec dataSpec, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, long startTimeUs, long endTimeUs, long clippedStartTimeUs, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java index 2e7581eba5..a794f67fe2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java @@ -56,7 +56,7 @@ public abstract class Chunk implements Loadable { * Optional data associated with the selection of the track to which this chunk belongs. Null if * the chunk does not belong to a track. */ - public final @Nullable Object trackSelectionData; + @Nullable public final Object trackSelectionData; /** * The start time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data * being loaded does not contain media samples. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java index 6b7f5688ae..d6400c5165 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java @@ -15,15 +15,15 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; + /** * Holds a chunk or an indication that the end of the stream has been reached. */ public final class ChunkHolder { - /** - * The chunk. - */ - public Chunk chunk; + /** The chunk. */ + @Nullable public Chunk chunk; /** * Indicates that the end of the stream has been reached. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 18eada4708..6eaeefec6b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -21,12 +21,14 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.source.DecryptableSampleQueueReader; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; @@ -60,8 +62,8 @@ public class ChunkSampleStream implements SampleStream, S public final int primaryTrackType; - private final int[] embeddedTrackTypes; - private final Format[] embeddedTrackFormats; + @Nullable private final int[] embeddedTrackTypes; + @Nullable private final Format[] embeddedTrackFormats; private final boolean[] embeddedTracksSelected; private final T chunkSource; private final SequenceableLoader.Callback> callback; @@ -72,11 +74,12 @@ public class ChunkSampleStream implements SampleStream, S private final ArrayList mediaChunks; private final List readOnlyMediaChunks; private final SampleQueue primarySampleQueue; + private final DecryptableSampleQueueReader primarySampleQueueReader; private final SampleQueue[] embeddedSampleQueues; private final BaseMediaChunkOutput mediaChunkOutput; private Format primaryDownstreamTrackFormat; - private @Nullable ReleaseCallback releaseCallback; + @Nullable private ReleaseCallback releaseCallback; private long pendingResetPositionUs; private long lastSeekPositionUs; private int nextNotifyPrimaryFormatMediaChunkIndex; @@ -95,57 +98,20 @@ public class ChunkSampleStream implements SampleStream, S * @param callback An {@link Callback} for the stream. * @param allocator An {@link Allocator} from which allocations can be obtained. * @param positionUs The position from which to start loading media. - * @param minLoadableRetryCount The minimum number of times that the source should retry a load - * before propagating an error. - * @param eventDispatcher A dispatcher to notify of events. - * @deprecated Use {@link #ChunkSampleStream(int, int[], Format[], ChunkSource, Callback, - * Allocator, long, LoadErrorHandlingPolicy, EventDispatcher)} instead. - */ - @Deprecated - public ChunkSampleStream( - int primaryTrackType, - int[] embeddedTrackTypes, - Format[] embeddedTrackFormats, - T chunkSource, - Callback> callback, - Allocator allocator, - long positionUs, - int minLoadableRetryCount, - EventDispatcher eventDispatcher) { - this( - primaryTrackType, - embeddedTrackTypes, - embeddedTrackFormats, - chunkSource, - callback, - allocator, - positionUs, - new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), - eventDispatcher); - } - - /** - * Constructs an instance. - * - * @param primaryTrackType The type of the primary track. One of the {@link C} {@code - * TRACK_TYPE_*} constants. - * @param embeddedTrackTypes The types of any embedded tracks, or null. - * @param embeddedTrackFormats The formats of the embedded tracks, or null. - * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. - * @param callback An {@link Callback} for the stream. - * @param allocator An {@link Allocator} from which allocations can be obtained. - * @param positionUs The position from which to start loading media. + * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} + * from. * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. */ public ChunkSampleStream( int primaryTrackType, - int[] embeddedTrackTypes, - Format[] embeddedTrackFormats, + @Nullable int[] embeddedTrackTypes, + @Nullable Format[] embeddedTrackFormats, T chunkSource, Callback> callback, Allocator allocator, long positionUs, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher) { this.primaryTrackType = primaryTrackType; @@ -167,6 +133,8 @@ public class ChunkSampleStream implements SampleStream, S SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; primarySampleQueue = new SampleQueue(allocator); + primarySampleQueueReader = + new DecryptableSampleQueueReader(primarySampleQueue, drmSessionManager); trackTypes[0] = primaryTrackType; sampleQueues[0] = primarySampleQueue; @@ -369,6 +337,7 @@ public class ChunkSampleStream implements SampleStream, S this.releaseCallback = callback; // Discard as much as we can synchronously. primarySampleQueue.discardToEnd(); + primarySampleQueueReader.release(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.discardToEnd(); } @@ -390,12 +359,13 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && primarySampleQueue.hasNextSample()); + return !isPendingReset() && primarySampleQueueReader.isReady(loadingFinished); } @Override public void maybeThrowError() throws IOException { loader.maybeThrowError(); + primarySampleQueueReader.maybeThrowError(); if (!loader.isLoading()) { chunkSource.maybeThrowError(); } @@ -408,7 +378,8 @@ public class ChunkSampleStream implements SampleStream, S return C.RESULT_NOTHING_READ; } maybeNotifyPrimaryTrackFormatChanged(); - return primarySampleQueue.read( + + return primarySampleQueueReader.read( formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); } @@ -801,7 +772,12 @@ public class ChunkSampleStream implements SampleStream, S } maybeNotifyDownstreamFormat(); return sampleQueue.read( - formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); + formatHolder, + buffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + loadingFinished, + decodeOnlyUntilPositionUs); } public void release() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java index 9626f4b03f..39c097826f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.upstream.DataSource; @@ -44,7 +45,7 @@ public abstract class MediaChunk extends Chunk { DataSpec dataSpec, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, long startTimeUs, long endTimeUs, long chunkIndex) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java b/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java index b863d80c9a..a7ab93a6dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java @@ -19,6 +19,7 @@ import android.annotation.TargetApi; import android.graphics.Color; import android.graphics.Typeface; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import android.view.accessibility.CaptioningManager; import android.view.accessibility.CaptioningManager.CaptionStyle; import com.google.android.exoplayer2.util.Util; @@ -72,11 +73,15 @@ public final class CaptionStyleCompat { */ public static final int USE_TRACK_COLOR_SETTINGS = 1; - /** - * Default caption style. - */ - public static final CaptionStyleCompat DEFAULT = new CaptionStyleCompat( - Color.WHITE, Color.BLACK, Color.TRANSPARENT, EDGE_TYPE_NONE, Color.WHITE, null); + /** Default caption style. */ + public static final CaptionStyleCompat DEFAULT = + new CaptionStyleCompat( + Color.WHITE, + Color.BLACK, + Color.TRANSPARENT, + EDGE_TYPE_NONE, + Color.WHITE, + /* typeface= */ null); /** * The preferred foreground color. @@ -110,10 +115,8 @@ public final class CaptionStyleCompat { */ public final int edgeColor; - /** - * The preferred typeface. - */ - public final Typeface typeface; + /** The preferred typeface, or {@code null} if unspecified. */ + @Nullable public final Typeface typeface; /** * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}. @@ -141,8 +144,13 @@ public final class CaptionStyleCompat { * @param edgeColor See {@link #edgeColor}. * @param typeface See {@link #typeface}. */ - public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor, - @EdgeType int edgeType, int edgeColor, Typeface typeface) { + public CaptionStyleCompat( + int foregroundColor, + int backgroundColor, + int windowColor, + @EdgeType int edgeType, + int edgeColor, + @Nullable Typeface typeface) { this.foregroundColor = foregroundColor; this.backgroundColor = backgroundColor; this.windowColor = windowColor; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index 4b54b3ea9a..39359a9367 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text; import android.graphics.Bitmap; import android.graphics.Color; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import android.text.Layout.Alignment; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -28,9 +29,10 @@ import java.lang.annotation.RetentionPolicy; */ public class Cue { - /** - * An unset position or width. - */ + /** The empty cue. */ + public static final Cue EMPTY = new Cue(""); + + /** An unset position or width. */ public static final float DIMEN_UNSET = Float.MIN_VALUE; /** @@ -111,17 +113,13 @@ public class Cue { * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated * with styling spans. */ - public final CharSequence text; + @Nullable public final CharSequence text; - /** - * The alignment of the cue text within the cue box, or null if the alignment is undefined. - */ - public final Alignment textAlignment; + /** The alignment of the cue text within the cue box, or null if the alignment is undefined. */ + @Nullable public final Alignment textAlignment; - /** - * The cue image, or null if this is a text cue. - */ - public final Bitmap bitmap; + /** The cue image, or null if this is a text cue. */ + @Nullable public final Bitmap bitmap; /** * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction @@ -298,7 +296,7 @@ public class Cue { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -376,7 +374,7 @@ public class Cue { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -403,9 +401,9 @@ public class Cue { } private Cue( - CharSequence text, - Alignment textAlignment, - Bitmap bitmap, + @Nullable CharSequence text, + @Nullable Alignment textAlignment, + @Nullable Bitmap bitmap, float line, @LineType int lineType, @AnchorType int lineAnchor, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index 38d6ff25cb..bd561afaf8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.SimpleDecoder; import java.nio.ByteBuffer; @@ -69,6 +70,7 @@ public abstract class SimpleSubtitleDecoder extends @SuppressWarnings("ByteBufferBackingArray") @Override + @Nullable protected final SubtitleDecoderException decode( SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java index 75b7a01673..1dcdecf95f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.OutputBuffer; +import com.google.android.exoplayer2.util.Assertions; import java.util.List; /** @@ -24,7 +26,7 @@ import java.util.List; */ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subtitle { - private Subtitle subtitle; + @Nullable private Subtitle subtitle; private long subsampleOffsetUs; /** @@ -45,22 +47,22 @@ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subti @Override public int getEventTimeCount() { - return subtitle.getEventTimeCount(); + return Assertions.checkNotNull(subtitle).getEventTimeCount(); } @Override public long getEventTime(int index) { - return subtitle.getEventTime(index) + subsampleOffsetUs; + return Assertions.checkNotNull(subtitle).getEventTime(index) + subsampleOffsetUs; } @Override public int getNextEventTimeIndex(long timeUs) { - return subtitle.getNextEventTimeIndex(timeUs - subsampleOffsetUs); + return Assertions.checkNotNull(subtitle).getNextEventTimeIndex(timeUs - subsampleOffsetUs); } @Override public List getCues(long timeUs) { - return subtitle.getCues(timeUs - subsampleOffsetUs); + return Assertions.checkNotNull(subtitle).getCues(timeUs - subsampleOffsetUs); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 55bee5bd6a..1622d68d99 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -44,12 +44,6 @@ import java.util.List; */ public final class TextRenderer extends BaseRenderer implements Callback { - /** - * @deprecated Use {@link TextOutput}. - */ - @Deprecated - public interface Output extends TextOutput {} - @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -77,7 +71,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { private static final int MSG_UPDATE_OUTPUT = 0; - private final @Nullable Handler outputHandler; + @Nullable private final Handler outputHandler; private final TextOutput output; private final SubtitleDecoderFactory decoderFactory; private final FormatHolder formatHolder; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 774b94a43c..5a14063aa1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -242,7 +242,7 @@ public final class Cea608Decoder extends CeaDecoder { private int captionMode; private int captionRowCount; - private boolean captionValid; + private boolean isCaptionValid; private boolean repeatableControlSet; private byte repeatableControlCc1; private byte repeatableControlCc2; @@ -300,7 +300,7 @@ public final class Cea608Decoder extends CeaDecoder { setCaptionMode(CC_MODE_UNKNOWN); setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT); resetCueBuilders(); - captionValid = false; + isCaptionValid = false; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -358,13 +358,19 @@ public final class Cea608Decoder extends CeaDecoder { continue; } - boolean repeatedControlPossible = repeatableControlSet; - repeatableControlSet = false; + boolean previousIsCaptionValid = isCaptionValid; + isCaptionValid = + (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG + && ODD_PARITY_BYTE_TABLE[ccByte1] + && ODD_PARITY_BYTE_TABLE[ccByte2]; - boolean previousCaptionValid = captionValid; - captionValid = (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG; - if (!captionValid) { - if (previousCaptionValid) { + if (isRepeatedCommand(isCaptionValid, ccData1, ccData2)) { + // Ignore repeated valid commands. + continue; + } + + if (!isCaptionValid) { + if (previousIsCaptionValid) { // The encoder has flipped the validity bit to indicate captions are being turned off. resetCueBuilders(); captionDataProcessed = true; @@ -372,65 +378,41 @@ public final class Cea608Decoder extends CeaDecoder { continue; } - // If we've reached this point then there is data to process; flag that work has been done. - captionDataProcessed = true; - - if (!ODD_PARITY_BYTE_TABLE[ccByte1] || !ODD_PARITY_BYTE_TABLE[ccByte2]) { - // The data is invalid. - resetCueBuilders(); - continue; - } - maybeUpdateIsInCaptionService(ccData1, ccData2); if (!isInCaptionService) { // Only the Captioning service is supported. Drop all other bytes. continue; } - // Special North American character set. - // ccData1 - 0|0|0|1|C|0|0|1 - // ccData2 - 0|0|1|1|X|X|X|X - if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) { - if (getChannel(ccData1) == selectedChannel) { - currentCueBuilder.append(getSpecialChar(ccData2)); - } + if (!updateAndVerifyCurrentChannel(ccData1)) { + // Wrong channel. continue; } - // Extended Western European character set. - // ccData1 - 0|0|0|1|C|0|1|S - // ccData2 - 0|0|1|X|X|X|X|X - if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) { - if (getChannel(ccData1) == selectedChannel) { - // Remove standard equivalent of the special extended char before appending new one + if (isCtrlCode(ccData1)) { + if (isSpecialNorthAmericanChar(ccData1, ccData2)) { + currentCueBuilder.append(getSpecialNorthAmericanChar(ccData2)); + } else if (isExtendedWestEuropeanChar(ccData1, ccData2)) { + // Remove standard equivalent of the special extended char before appending new one. currentCueBuilder.backspace(); - if ((ccData1 & 0x01) == 0x00) { - // Extended Spanish/Miscellaneous and French character set (S = 0). - currentCueBuilder.append(getExtendedEsFrChar(ccData2)); - } else { - // Extended Portuguese and German/Danish character set (S = 1). - currentCueBuilder.append(getExtendedPtDeChar(ccData2)); - } + currentCueBuilder.append(getExtendedWestEuropeanChar(ccData1, ccData2)); + } else if (isMidrowCtrlCode(ccData1, ccData2)) { + handleMidrowCtrl(ccData2); + } else if (isPreambleAddressCode(ccData1, ccData2)) { + handlePreambleAddressCode(ccData1, ccData2); + } else if (isTabCtrlCode(ccData1, ccData2)) { + currentCueBuilder.tabOffset = ccData2 - 0x20; + } else if (isMiscCode(ccData1, ccData2)) { + handleMiscCode(ccData2); + } + } else { + // Basic North American character set. + currentCueBuilder.append(getBasicChar(ccData1)); + if ((ccData2 & 0xE0) != 0x00) { + currentCueBuilder.append(getBasicChar(ccData2)); } - continue; - } - - // Control character. - // ccData1 - 0|0|0|X|X|X|X|X - if ((ccData1 & 0xE0) == 0x00) { - handleCtrl(ccData1, ccData2, repeatedControlPossible); - continue; - } - - if (currentChannel != selectedChannel) { - continue; - } - - // Basic North American character set. - currentCueBuilder.append(getChar(ccData1)); - if ((ccData2 & 0xE0) != 0x00) { - currentCueBuilder.append(getChar(ccData2)); } + captionDataProcessed = true; } if (captionDataProcessed) { @@ -440,15 +422,22 @@ public final class Cea608Decoder extends CeaDecoder { } } - private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) { - currentChannel = getChannel(cc1); + private boolean updateAndVerifyCurrentChannel(byte cc1) { + if (isCtrlCode(cc1)) { + currentChannel = getChannel(cc1); + } + return currentChannel == selectedChannel; + } + + private boolean isRepeatedCommand(boolean captionValid, byte cc1, byte cc2) { // Most control commands are sent twice in succession to ensure they are received properly. We // don't want to process duplicate commands, so if we see the same repeatable command twice in a // row then we ignore the second one. - if (isRepeatable(cc1)) { - if (repeatedControlPossible && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) { + if (captionValid && isRepeatable(cc1)) { + if (repeatableControlSet && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) { // This is a repeated command, so we ignore it. - return; + repeatableControlSet = false; + return true; } else { // This is the first occurrence of a repeatable command. Set the repeatable control // variables so that we can recognize and ignore a duplicate (if there is one), and then @@ -457,21 +446,11 @@ public final class Cea608Decoder extends CeaDecoder { repeatableControlCc1 = cc1; repeatableControlCc2 = cc2; } + } else { + // This command is not repeatable. + repeatableControlSet = false; } - - if (currentChannel != selectedChannel) { - return; - } - - if (isMidrowCtrlCode(cc1, cc2)) { - handleMidrowCtrl(cc2); - } else if (isPreambleAddressCode(cc1, cc2)) { - handlePreambleAddressCode(cc1, cc2); - } else if (isTabCtrlCode(cc1, cc2)) { - currentCueBuilder.tabOffset = cc2 - 0x20; - } else if (isMiscCode(cc1, cc2)) { - handleMiscCode(cc2); - } + return false; } private void handleMidrowCtrl(byte cc2) { @@ -676,16 +655,38 @@ public final class Cea608Decoder extends CeaDecoder { } } - private static char getChar(byte ccData) { + private static char getBasicChar(byte ccData) { int index = (ccData & 0x7F) - 0x20; return (char) BASIC_CHARACTER_SET[index]; } - private static char getSpecialChar(byte ccData) { + private static boolean isSpecialNorthAmericanChar(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|0|0|1 + // cc2 - 0|0|1|1|X|X|X|X + return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x30); + } + + private static char getSpecialNorthAmericanChar(byte ccData) { int index = ccData & 0x0F; return (char) SPECIAL_CHARACTER_SET[index]; } + private static boolean isExtendedWestEuropeanChar(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|0|1|S + // cc2 - 0|0|1|X|X|X|X|X + return ((cc1 & 0xF6) == 0x12) && ((cc2 & 0xE0) == 0x20); + } + + private static char getExtendedWestEuropeanChar(byte cc1, byte cc2) { + if ((cc1 & 0x01) == 0x00) { + // Extended Spanish/Miscellaneous and French character set (S = 0). + return getExtendedEsFrChar(cc2); + } else { + // Extended Portuguese and German/Danish character set (S = 1). + return getExtendedPtDeChar(cc2); + } + } + private static char getExtendedEsFrChar(byte ccData) { int index = ccData & 0x1F; return (char) SPECIAL_ES_FR_CHARACTER_SET[index]; @@ -696,6 +697,11 @@ public final class Cea608Decoder extends CeaDecoder { return (char) SPECIAL_PT_DE_CHARACTER_SET[index]; } + private static boolean isCtrlCode(byte cc1) { + // cc1 - 0|0|0|X|X|X|X|X + return (cc1 & 0xE0) == 0x00; + } + private static int getChannel(byte cc1) { // cc1 - X|X|X|X|C|X|X|X return (cc1 >> 3) & 0x1; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java index 75fe8fed25..cdc545e459 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java @@ -19,14 +19,13 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; /** Utility methods for handling CEA-608/708 messages. Defined in A/53 Part 4:2009. */ public final class CeaUtil { private static final String TAG = "CeaUtil"; - public static final int USER_DATA_IDENTIFIER_GA94 = Util.getIntegerCodeForString("GA94"); + public static final int USER_DATA_IDENTIFIER_GA94 = 0x47413934; public static final int USER_DATA_TYPE_CODE_MPEG_CC = 0x3; private static final int PAYLOAD_TYPE_CC = 4; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java index df5b19c052..22ce893fce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.dvb; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; @@ -38,7 +39,7 @@ public final class DvbDecoder extends SimpleSubtitleDecoder { } @Override - protected DvbSubtitle decode(byte[] data, int length, boolean reset) { + protected Subtitle decode(byte[] data, int length, boolean reset) { if (reset) { parser.reset(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java index 091bda49f3..9ef3556c8f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.pgs; import android.graphics.Bitmap; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; @@ -41,7 +42,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { private final ParsableByteArray inflatedBuffer; private final CueBuilder cueBuilder; - private Inflater inflater; + @Nullable private Inflater inflater; public PgsDecoder() { super("PgsDecoder"); @@ -76,6 +77,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { } } + @Nullable private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { int limit = buffer.limit(); int sectionType = buffer.readUnsignedByte(); @@ -197,6 +199,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { bitmapY = buffer.readUnsignedShort(); } + @Nullable public Cue build() { if (planeWidth == 0 || planeHeight == 0 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index c25b26128c..e305259cbc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -15,10 +15,12 @@ */ package com.google.android.exoplayer2.text.ssa; +import androidx.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.LongArray; @@ -49,7 +51,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { private int formatTextIndex; public SsaDecoder() { - this(null); + this(/* initializationData= */ null); } /** @@ -58,7 +60,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * format line. The second must contain an SSA header that will be assumed common to all * samples. */ - public SsaDecoder(List initializationData) { + public SsaDecoder(@Nullable List initializationData) { super("SsaDecoder"); if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; @@ -72,7 +74,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } @Override - protected SsaSubtitle decode(byte[] bytes, int length, boolean reset) { + protected Subtitle decode(byte[] bytes, int length, boolean reset) { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); @@ -201,7 +203,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { cues.add(new Cue(text)); cueTimesUs.add(startTimeUs); if (endTimeUs != C.TIME_UNSET) { - cues.add(null); + cues.add(Cue.EMPTY); cueTimesUs.add(endTimeUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java index 339119ed6b..9a3756194f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java @@ -32,7 +32,7 @@ import java.util.List; private final long[] cueTimesUs; /** - * @param cues The cues in the subtitle. Null entries may be used to represent empty cues. + * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { @@ -61,7 +61,7 @@ import java.util.List; @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == null) { + if (index == -1 || cues[index] == Cue.EMPTY) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.emptyList(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 6f9fd366ec..eb2b704bee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -21,6 +21,7 @@ import android.text.Spanned; import android.text.TextUtils; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -34,9 +35,9 @@ import java.util.regex.Pattern; public final class SubripDecoder extends SimpleSubtitleDecoder { // Fractional positions for use when alignment tags are present. - /* package */ static final float START_FRACTION = 0.08f; - /* package */ static final float END_FRACTION = 1 - START_FRACTION; - /* package */ static final float MID_FRACTION = 0.5f; + private static final float START_FRACTION = 0.08f; + private static final float END_FRACTION = 1 - START_FRACTION; + private static final float MID_FRACTION = 0.5f; private static final String TAG = "SubripDecoder"; @@ -68,7 +69,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } @Override - protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) { + protected Subtitle decode(byte[] bytes, int length, boolean reset) { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); @@ -111,11 +112,13 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { // Read and parse the text and tags. textBuilder.setLength(0); tags.clear(); - while (!TextUtils.isEmpty(currentLine = subripData.readLine())) { + currentLine = subripData.readLine(); + while (!TextUtils.isEmpty(currentLine)) { if (textBuilder.length() > 0) { textBuilder.append("
    "); } textBuilder.append(processLine(currentLine, tags)); + currentLine = subripData.readLine(); } Spanned text = Html.fromHtml(textBuilder.toString()); @@ -132,7 +135,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { cues.add(buildCue(text, alignmentTag)); if (haveEndTimecode) { - cues.add(null); + cues.add(Cue.EMPTY); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java index a79df478e5..01ed1711a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java @@ -32,7 +32,7 @@ import java.util.List; private final long[] cueTimesUs; /** - * @param cues The cues in the subtitle. Null entries may be used to represent empty cues. + * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ public SubripSubtitle(Cue[] cues, long[] cueTimesUs) { @@ -61,7 +61,7 @@ import java.util.List; @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == null) { + if (index == -1 || cues[index] == Cue.EMPTY) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.emptyList(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index b39f467968..6dabcdd904 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -19,6 +19,7 @@ import android.text.Layout; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.Log; @@ -102,7 +103,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } @Override - protected TtmlSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { try { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); @@ -429,6 +430,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { /* lineType= */ Cue.LINE_TYPE_FRACTION, lineAnchor, width, + height, /* textSizeType= */ Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING, /* textSize= */ regionTextHeight); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index ecf5c8b0a0..3b4d061aaa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -231,11 +231,11 @@ import java.util.TreeSet; new Cue( bitmap, region.position, - Cue.ANCHOR_TYPE_MIDDLE, + Cue.ANCHOR_TYPE_START, region.line, region.lineAnchor, region.width, - /* height= */ Cue.DIMEN_UNSET)); + region.height)); } // Create text based cues. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java index 2b1e9cf99a..3cbc25d4b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRegion.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.text.Cue; public final @Cue.LineType int lineType; public final @Cue.AnchorType int lineAnchor; public final float width; + public final float height; public final @Cue.TextSizeType int textSizeType; public final float textSize; @@ -39,6 +40,7 @@ import com.google.android.exoplayer2.text.Cue; /* lineType= */ Cue.TYPE_UNSET, /* lineAnchor= */ Cue.TYPE_UNSET, /* width= */ Cue.DIMEN_UNSET, + /* height= */ Cue.DIMEN_UNSET, /* textSizeType= */ Cue.TYPE_UNSET, /* textSize= */ Cue.DIMEN_UNSET); } @@ -50,6 +52,7 @@ import com.google.android.exoplayer2.text.Cue; @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float width, + float height, int textSizeType, float textSize) { this.id = id; @@ -58,6 +61,7 @@ import com.google.android.exoplayer2.text.Cue; this.lineType = lineType; this.lineAnchor = lineAnchor; this.width = width; + this.height = height; this.textSizeType = textSizeType; this.textSize = textSize; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index 9211dc51ce..c8f2979c58 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -43,8 +43,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { private static final char BOM_UTF16_BE = '\uFEFF'; private static final char BOM_UTF16_LE = '\uFFFE'; - private static final int TYPE_STYL = Util.getIntegerCodeForString("styl"); - private static final int TYPE_TBOX = Util.getIntegerCodeForString("tbox"); + private static final int TYPE_STYL = 0x7374796c; + private static final int TYPE_TBOX = 0x74626f78; private static final String TX3G_SERIF = "Serif"; private static final int SIZE_ATOM_HEADER = 8; @@ -65,6 +65,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f; private final ParsableByteArray parsableByteArray; + private boolean customVerticalPlacement; private int defaultFontFace; private int defaultColorRgba; @@ -80,10 +81,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { public Tx3gDecoder(List initializationData) { super("Tx3gDecoder"); parsableByteArray = new ParsableByteArray(); - decodeInitializationData(initializationData); - } - private void decodeInitializationData(List initializationData) { if (initializationData != null && initializationData.size() == 1 && (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) { byte[] initializationBytes = initializationData.get(0); @@ -151,8 +149,16 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { } parsableByteArray.setPosition(position + atomSize); } - return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION, - Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET)); + return new Tx3gSubtitle( + new Cue( + cueText, + /* textAlignment= */ null, + verticalPlacement, + Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_START, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + Cue.DIMEN_UNSET)); } private static String readSubtitleText(ParsableByteArray parsableByteArray) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java index 81c362bda5..c5d0526eb5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java @@ -15,11 +15,13 @@ */ package com.google.android.exoplayer2.text.webvtt; +import androidx.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -35,8 +37,8 @@ import java.util.regex.Pattern; private static final String PROPERTY_TEXT_DECORATION = "text-decoration"; private static final String VALUE_BOLD = "bold"; private static final String VALUE_UNDERLINE = "underline"; - private static final String BLOCK_START = "{"; - private static final String BLOCK_END = "}"; + private static final String RULE_START = "{"; + private static final String RULE_END = "}"; private static final String PROPERTY_FONT_STYLE = "font-style"; private static final String VALUE_ITALIC = "italic"; @@ -52,47 +54,58 @@ import java.util.regex.Pattern; } /** - * Takes a CSS style block and consumes up to the first empty line found. Attempts to parse the - * contents of the style block and returns a {@link WebvttCssStyle} instance if successful, or - * {@code null} otherwise. + * Takes a CSS style block and consumes up to the first empty line. Attempts to parse the contents + * of the style block and returns a list of {@link WebvttCssStyle} instances if successful. If + * parsing fails, it returns a list including only the styles which have been successfully parsed + * up to the style rule which was malformed. * * @param input The input from which the style block should be read. - * @return A {@link WebvttCssStyle} that represents the parsed block. + * @return A list of {@link WebvttCssStyle}s that represents the parsed block, or a list + * containing the styles up to the parsing failure. */ - public WebvttCssStyle parseBlock(ParsableByteArray input) { + public List parseBlock(ParsableByteArray input) { stringBuilder.setLength(0); int initialInputPosition = input.getPosition(); skipStyleBlock(input); styleInput.reset(input.data, input.getPosition()); styleInput.setPosition(initialInputPosition); - String selector = parseSelector(styleInput, stringBuilder); - if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) { - return null; - } - WebvttCssStyle style = new WebvttCssStyle(); - applySelectorToStyle(style, selector); - String token = null; - boolean blockEndFound = false; - while (!blockEndFound) { - int position = styleInput.getPosition(); - token = parseNextToken(styleInput, stringBuilder); - blockEndFound = token == null || BLOCK_END.equals(token); - if (!blockEndFound) { - styleInput.setPosition(position); - parseStyleDeclaration(styleInput, style, stringBuilder); + + List styles = new ArrayList<>(); + String selector; + while ((selector = parseSelector(styleInput, stringBuilder)) != null) { + if (!RULE_START.equals(parseNextToken(styleInput, stringBuilder))) { + return styles; + } + WebvttCssStyle style = new WebvttCssStyle(); + applySelectorToStyle(style, selector); + String token = null; + boolean blockEndFound = false; + while (!blockEndFound) { + int position = styleInput.getPosition(); + token = parseNextToken(styleInput, stringBuilder); + blockEndFound = token == null || RULE_END.equals(token); + if (!blockEndFound) { + styleInput.setPosition(position); + parseStyleDeclaration(styleInput, style, stringBuilder); + } + } + // Check that the style rule ended correctly. + if (RULE_END.equals(token)) { + styles.add(style); } } - return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly. + return styles; } /** - * Returns a string containing the selector. The input is expected to have the form - * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. + * Returns a string containing the selector. The input is expected to have the form {@code + * ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. * * @param input From which the selector is obtained. - * @return A string containing the target, empty string if the selector is universal - * (targets all cues) or null if an error was encountered. + * @return A string containing the target, empty string if the selector is universal (targets all + * cues) or null if an error was encountered. */ + @Nullable private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() < 5) { @@ -107,7 +120,7 @@ import java.util.regex.Pattern; if (token == null) { return null; } - if (BLOCK_START.equals(token)) { + if (RULE_START.equals(token)) { input.setPosition(position); return ""; } @@ -116,7 +129,7 @@ import java.util.regex.Pattern; target = readCueTarget(input); } token = parseNextToken(input, stringBuilder); - if (!")".equals(token) || token == null) { + if (!")".equals(token)) { return null; } return target; @@ -156,7 +169,7 @@ import java.util.regex.Pattern; String token = parseNextToken(input, stringBuilder); if (";".equals(token)) { // The style declaration is well formed. - } else if (BLOCK_END.equals(token)) { + } else if (RULE_END.equals(token)) { // The style declaration is well formed and we can go on, but the closing bracket had to be // fed back. input.setPosition(position); @@ -196,6 +209,7 @@ import java.util.regex.Pattern; } // Visible for testing. + @Nullable /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() == 0) { @@ -237,6 +251,7 @@ import java.util.regex.Pattern; return (char) input.data[position]; } + @Nullable private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) { StringBuilder expressionBuilder = new StringBuilder(); String token; @@ -250,7 +265,7 @@ import java.util.regex.Pattern; // Syntax error. return null; } - if (BLOCK_END.equals(token) || ";".equals(token)) { + if (RULE_END.equals(token) || ";".equals(token)) { input.setPosition(position); expressionEndFound = true; } else { @@ -325,7 +340,7 @@ import java.util.regex.Pattern; style.setTargetTagName(tagAndIdDivision); } if (classDivision.length > 1) { - style.setTargetClasses(Arrays.copyOfRange(classDivision, 1, classDivision.length)); + style.setTargetClasses(Util.nullSafeArrayCopyOfRange(classDivision, 1, classDivision.length)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java index 8cb0ac58c7..8b255ac2bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.text.webvtt; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -24,16 +25,20 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -/** - * A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file. - */ +/** A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file. */ +@SuppressWarnings("ConstantField") public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { private static final int BOX_HEADER_SIZE = 8; - private static final int TYPE_payl = Util.getIntegerCodeForString("payl"); - private static final int TYPE_sttg = Util.getIntegerCodeForString("sttg"); - private static final int TYPE_vttc = Util.getIntegerCodeForString("vttc"); + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_payl = 0x7061796c; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_sttg = 0x73747467; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_vttc = 0x76747463; private final ParsableByteArray sampleData; private final WebvttCue.Builder builder; @@ -45,7 +50,7 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected Mp4WebvttSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing: // first 4 bytes size and then 4 bytes type. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java index 06d3c14970..9b356f0988 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; @@ -55,7 +56,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected WebvttSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { parsableWebvttData.reset(bytes, length); // Initialization for consistent starting state. @@ -80,10 +81,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { throw new SubtitleDecoderException("A style block was found after the first cue."); } parsableWebvttData.readLine(); // Consume the "STYLE" header. - WebvttCssStyle styleBlock = cssParser.parseBlock(parsableWebvttData); - if (styleBlock != null) { - definedStyles.add(styleBlock); - } + definedStyles.addAll(cssParser.parseBlock(parsableWebvttData)); } else if (event == EVENT_CUE) { if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) { subtitles.add(webvttCueBuilder.build()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index bbf57c5602..c5d22c15cb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -39,7 +39,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** Factory for {@link AdaptiveTrackSelection} instances. */ public static class Factory implements TrackSelection.Factory { - private final @Nullable BandwidthMeter bandwidthMeter; + @Nullable private final BandwidthMeter bandwidthMeter; private final int minDurationForQualityIncreaseMs; private final int maxDurationForQualityDecreaseMs; private final int minDurationToRetainAfterDiscardMs; @@ -49,7 +49,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final Clock clock; private TrackBitrateEstimator trackBitrateEstimator; - private boolean blockFixedTrackSelectionBandwidth; /** Creates an adaptive track selection factory with default parameters. */ public Factory() { @@ -218,15 +217,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.trackBitrateEstimator = trackBitrateEstimator; } - /** - * Enables blocking of the total fixed track selection bandwidth. - * - *

    This method is experimental, and will be renamed or removed in a future release. - */ - public final void experimental_enableBlockFixedTrackSelectionBandwidth() { - this.blockFixedTrackSelectionBandwidth = true; - } - @Override public final @NullableType TrackSelection[] createTrackSelections( @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { @@ -234,20 +224,11 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { bandwidthMeter = this.bandwidthMeter; } TrackSelection[] selections = new TrackSelection[definitions.length]; - List adaptiveSelections = new ArrayList<>(); int totalFixedBandwidth = 0; for (int i = 0; i < definitions.length; i++) { Definition definition = definitions[i]; - if (definition == null) { - continue; - } - if (definition.tracks.length > 1) { - AdaptiveTrackSelection adaptiveSelection = - createAdaptiveTrackSelection(definition.group, bandwidthMeter, definition.tracks); - adaptiveSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator); - adaptiveSelections.add(adaptiveSelection); - selections[i] = adaptiveSelection; - } else { + if (definition != null && definition.tracks.length == 1) { + // Make fixed selections first to know their total bandwidth. selections[i] = new FixedTrackSelection( definition.group, definition.tracks[0], definition.reason, definition.data); @@ -257,9 +238,16 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } } } - if (blockFixedTrackSelectionBandwidth) { - for (int i = 0; i < adaptiveSelections.size(); i++) { - adaptiveSelections.get(i).experimental_setNonAllocatableBandwidth(totalFixedBandwidth); + List adaptiveSelections = new ArrayList<>(); + for (int i = 0; i < definitions.length; i++) { + Definition definition = definitions[i]; + if (definition != null && definition.tracks.length > 1) { + AdaptiveTrackSelection adaptiveSelection = + createAdaptiveTrackSelection( + definition.group, bandwidthMeter, definition.tracks, totalFixedBandwidth); + adaptiveSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator); + adaptiveSelections.add(adaptiveSelection); + selections[i] = adaptiveSelection; } } if (adaptiveSelections.size() > 1) { @@ -288,14 +276,19 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param group The {@link TrackGroup}. * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. * @param tracks The indices of the selected tracks in the track group. + * @param totalFixedTrackBandwidth The total bandwidth used by all non-adaptive tracks, in bits + * per second. * @return An {@link AdaptiveTrackSelection} for the specified tracks. */ protected AdaptiveTrackSelection createAdaptiveTrackSelection( - TrackGroup group, BandwidthMeter bandwidthMeter, int[] tracks) { + TrackGroup group, + BandwidthMeter bandwidthMeter, + int[] tracks, + int totalFixedTrackBandwidth) { return new AdaptiveTrackSelection( group, tracks, - new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction), + new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, totalFixedTrackBandwidth), minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, @@ -341,6 +334,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { group, tracks, bandwidthMeter, + /* reservedBandwidth= */ 0, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -355,6 +349,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param reservedBandwidth The reserved bandwidth, which shouldn't be considered available for + * use, in bits per second. * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the * selected track to switch to one of higher quality. * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the @@ -381,6 +377,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, + long reservedBandwidth, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, @@ -391,7 +388,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this( group, tracks, - new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction), + new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, reservedBandwidth), minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, @@ -445,18 +442,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.trackBitrateEstimator = trackBitrateEstimator; } - /** - * Sets the non-allocatable bandwidth, which shouldn't be considered available. - * - *

    This method is experimental, and will be renamed or removed in a future release. - * - * @param nonAllocatableBandwidth The non-allocatable bandwidth in bits per second. - */ - public void experimental_setNonAllocatableBandwidth(long nonAllocatableBandwidth) { - ((DefaultBandwidthProvider) bandwidthProvider) - .experimental_setNonAllocatableBandwidth(nonAllocatableBandwidth); - } - /** * Sets checkpoints to determine the allocation bandwidth based on the total bandwidth. * @@ -537,7 +522,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } @Override - public @Nullable Object getSelectionData() { + @Nullable + public Object getSelectionData() { return null; } @@ -665,20 +651,21 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final BandwidthMeter bandwidthMeter; private final float bandwidthFraction; - - private long nonAllocatableBandwidth; + private final long reservedBandwidth; @Nullable private long[][] allocationCheckpoints; - /* package */ DefaultBandwidthProvider(BandwidthMeter bandwidthMeter, float bandwidthFraction) { + /* package */ DefaultBandwidthProvider( + BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) { this.bandwidthMeter = bandwidthMeter; this.bandwidthFraction = bandwidthFraction; + this.reservedBandwidth = reservedBandwidth; } @Override public long getAllocatedBandwidth() { long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); - long allocatableBandwidth = Math.max(0L, totalBandwidth - nonAllocatableBandwidth); + long allocatableBandwidth = Math.max(0L, totalBandwidth - reservedBandwidth); if (allocationCheckpoints == null) { return allocatableBandwidth; } @@ -694,10 +681,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { return previous[1] + (long) (fractionBetweenCheckpoints * (next[1] - previous[1])); } - /* package */ void experimental_setNonAllocatableBandwidth(long nonAllocatableBandwidth) { - this.nonAllocatableBandwidth = nonAllocatableBandwidth; - } - /* package */ void experimental_setBandwidthAllocationCheckpoints( long[][] allocationCheckpoints) { Assertions.checkArgument(allocationCheckpoints.length >= 2); @@ -757,7 +740,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { for (int i = 0; i < values.length; i++) { logValues[i] = new double[values[i].length]; for (int j = 0; j < values[i].length; j++) { - logValues[i][j] = Math.log(values[i][j]); + logValues[i][j] = values[i][j] == Format.NO_VALUE ? 0 : Math.log(values[i][j]); } } return logValues; @@ -779,7 +762,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0]; for (int j = 0; j < logBitrates[i].length - 1; j++) { double switchBitrate = 0.5 * (logBitrates[i][j] + logBitrates[i][j + 1]); - switchPoints[i][j] = (switchBitrate - logBitrates[i][0]) / totalBitrateDiff; + switchPoints[i][j] = + totalBitrateDiff == 0.0 ? 1.0 : (switchBitrate - logBitrates[i][0]) / totalBitrateDiff; } } return switchPoints; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 3200e40495..8e1284f7ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -19,7 +19,6 @@ import android.content.Context; import android.graphics.Point; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; @@ -178,6 +177,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean exceedAudioConstraintsIfNecessary; private boolean allowAudioMixedMimeTypeAdaptiveness; private boolean allowAudioMixedSampleRateAdaptiveness; + private boolean allowAudioMixedChannelCountAdaptiveness; // General private boolean forceLowestBitrate; private boolean forceHighestSupportedBitrate; @@ -187,9 +187,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final SparseArray> selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; - /** Creates a builder with default initial values. */ + /** + * @deprecated Initial viewport constraints will not be set based on the primary display when + * using this constructor. Use {@link #ParametersBuilder(Context)} instead. + */ + @Deprecated public ParametersBuilder() { - this(Parameters.DEFAULT); + this(Parameters.DEFAULT_WITHOUT_VIEWPORT); + } + + /** + * Creates a builder with default initial values. + * + * @param context Any context. + */ + public ParametersBuilder(Context context) { + this(Parameters.getDefaults(context)); } /** @@ -215,6 +228,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { exceedAudioConstraintsIfNecessary = initialValues.exceedAudioConstraintsIfNecessary; allowAudioMixedMimeTypeAdaptiveness = initialValues.allowAudioMixedMimeTypeAdaptiveness; allowAudioMixedSampleRateAdaptiveness = initialValues.allowAudioMixedSampleRateAdaptiveness; + allowAudioMixedChannelCountAdaptiveness = + initialValues.allowAudioMixedChannelCountAdaptiveness; // General forceLowestBitrate = initialValues.forceLowestBitrate; forceHighestSupportedBitrate = initialValues.forceHighestSupportedBitrate; @@ -246,8 +261,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxVideoWidth} and {@link Parameters#maxVideoHeight}. + * Sets the maximum allowed video width and height. * + * @param maxVideoWidth Maximum allowed video width in pixels. + * @param maxVideoHeight Maximum allowed video height in pixels. * @return This builder. */ public ParametersBuilder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { @@ -257,8 +274,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxVideoFrameRate}. + * Sets the maximum allowed video frame rate. * + * @param maxVideoFrameRate Maximum allowed video frame rate in hertz. * @return This builder. */ public ParametersBuilder setMaxVideoFrameRate(int maxVideoFrameRate) { @@ -267,8 +285,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxVideoBitrate}. + * Sets the maximum allowed video bitrate. * + * @param maxVideoBitrate Maximum allowed video bitrate in bits per second. * @return This builder. */ public ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) { @@ -277,8 +296,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#exceedVideoConstraintsIfNecessary}. + * Sets whether to exceed the {@link #setMaxVideoSize(int, int)} and {@link + * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise. * + * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no + * selection can be made otherwise. * @return This builder. */ public ParametersBuilder setExceedVideoConstraintsIfNecessary( @@ -288,8 +310,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowVideoMixedMimeTypeAdaptiveness}. + * Sets whether to allow adaptive video selections containing mixed MIME types. * + *

    Adaptations between different MIME types may not be completely seamless, in which case + * {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} also needs to be {@code true} for + * mixed MIME type selections to be made. + * + * @param allowVideoMixedMimeTypeAdaptiveness Whether to allow adaptive video selections + * containing mixed MIME types. * @return This builder. */ public ParametersBuilder setAllowVideoMixedMimeTypeAdaptiveness( @@ -299,8 +327,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowVideoNonSeamlessAdaptiveness}. + * Sets whether to allow adaptive video selections where adaptation may not be completely + * seamless. * + * @param allowVideoNonSeamlessAdaptiveness Whether to allow adaptive video selections where + * adaptation may not be completely seamless. * @return This builder. */ public ParametersBuilder setAllowVideoNonSeamlessAdaptiveness( @@ -314,7 +345,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * obtained from {@link Util#getPhysicalDisplaySize(Context)}. * * @param context Any context. - * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. + * @param viewportOrientationMayChange Whether the viewport orientation may change during + * playback. * @return This builder. */ public ParametersBuilder setViewportSizeToPhysicalDisplaySize( @@ -335,12 +367,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and {@link - * Parameters#viewportOrientationMayChange}. + * Sets the viewport size to constrain adaptive video selections so that only tracks suitable + * for the viewport are selected. * - * @param viewportWidth See {@link Parameters#viewportWidth}. - * @param viewportHeight See {@link Parameters#viewportHeight}. - * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. + * @param viewportWidth Viewport width in pixels. + * @param viewportHeight Viewport height in pixels. + * @param viewportOrientationMayChange Whether the viewport orientation may change during + * playback. * @return This builder. */ public ParametersBuilder setViewportSize( @@ -360,8 +393,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxAudioChannelCount}. + * Sets the maximum allowed audio channel count. * + * @param maxAudioChannelCount Maximum allowed audio channel count. * @return This builder. */ public ParametersBuilder setMaxAudioChannelCount(int maxAudioChannelCount) { @@ -370,8 +404,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#maxAudioBitrate}. + * Sets the maximum allowed audio bitrate. * + * @param maxAudioBitrate Maximum allowed audio bitrate in bits per second. * @return This builder. */ public ParametersBuilder setMaxAudioBitrate(int maxAudioBitrate) { @@ -380,8 +415,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#exceedAudioConstraintsIfNecessary}. + * Sets whether to exceed the {@link #setMaxAudioChannelCount(int)} and {@link + * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise. * + * @param exceedAudioConstraintsIfNecessary Whether to exceed audio constraints when no + * selection can be made otherwise. * @return This builder. */ public ParametersBuilder setExceedAudioConstraintsIfNecessary( @@ -391,8 +429,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowAudioMixedMimeTypeAdaptiveness}. + * Sets whether to allow adaptive audio selections containing mixed MIME types. * + *

    Adaptations between different MIME types may not be completely seamless. + * + * @param allowAudioMixedMimeTypeAdaptiveness Whether to allow adaptive audio selections + * containing mixed MIME types. * @return This builder. */ public ParametersBuilder setAllowAudioMixedMimeTypeAdaptiveness( @@ -402,8 +444,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#allowAudioMixedSampleRateAdaptiveness}. + * Sets whether to allow adaptive audio selections containing mixed sample rates. * + *

    Adaptations between different sample rates may not be completely seamless. + * + * @param allowAudioMixedSampleRateAdaptiveness Whether to allow adaptive audio selections + * containing mixed sample rates. * @return This builder. */ public ParametersBuilder setAllowAudioMixedSampleRateAdaptiveness( @@ -412,6 +458,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + /** + * Sets whether to allow adaptive audio selections containing mixed channel counts. + * + *

    Adaptations between different channel counts may not be completely seamless. + * + * @param allowAudioMixedChannelCountAdaptiveness Whether to allow adaptive audio selections + * containing mixed channel counts. + * @return This builder. + */ + public ParametersBuilder setAllowAudioMixedChannelCountAdaptiveness( + boolean allowAudioMixedChannelCountAdaptiveness) { + this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; + return this; + } + // Text @Override @@ -436,8 +497,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { // General /** - * See {@link Parameters#forceLowestBitrate}. + * Sets whether to force selection of the single lowest bitrate audio and video tracks that + * comply with all other constraints. * + * @param forceLowestBitrate Whether to force selection of the single lowest bitrate audio and + * video tracks. * @return This builder. */ public ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) { @@ -446,8 +510,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#forceHighestSupportedBitrate}. + * Sets whether to force selection of the highest bitrate audio and video tracks that comply + * with all other constraints. * + * @param forceHighestSupportedBitrate Whether to force selection of the highest bitrate audio + * and video tracks. * @return This builder. */ public ParametersBuilder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) { @@ -473,8 +540,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#exceedRendererCapabilitiesIfNecessary}. + * Sets whether to exceed renderer capabilities when no selection can be made otherwise. * + *

    This parameter applies when all of the tracks available for a renderer exceed the + * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality + * track will still be selected. Playback may succeed if the renderer has under-reported its + * true capabilities. If {@code false} then no track will be selected. + * + * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no + * selection can be made otherwise. * @return This builder. */ public ParametersBuilder setExceedRendererCapabilitiesIfNecessary( @@ -484,7 +558,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#tunnelingAudioSessionId}. + * Sets the audio session id to use when tunneling. * *

    Enables or disables tunneling. To enable tunneling, pass an audio session id to use when * in tunneling mode. Session ids can be generated using {@link @@ -494,6 +568,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link * C#AUDIO_SESSION_ID_UNSET} to disable tunneling. + * @return This builder. */ public ParametersBuilder setTunnelingAudioSessionId(int tunnelingAudioSessionId) { this.tunnelingAudioSessionId = tunnelingAudioSessionId; @@ -508,6 +583,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param rendererIndex The renderer index. * @param disabled Whether the renderer is disabled. + * @return This builder. */ public final ParametersBuilder setRendererDisabled(int rendererIndex, boolean disabled) { if (rendererDisabledFlags.get(rendererIndex) == disabled) { @@ -544,6 +620,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray} for which the override should be applied. * @param override The override. + * @return This builder. */ public final ParametersBuilder setSelectionOverride( int rendererIndex, TrackGroupArray groups, SelectionOverride override) { @@ -565,6 +642,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray} for which the override should be cleared. + * @return This builder. */ public final ParametersBuilder clearSelectionOverride( int rendererIndex, TrackGroupArray groups) { @@ -584,6 +662,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Clears all track selection overrides for the specified renderer. * * @param rendererIndex The renderer index. + * @return This builder. */ public final ParametersBuilder clearSelectionOverrides(int rendererIndex) { Map overrides = selectionOverrides.get(rendererIndex); @@ -595,7 +674,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } - /** Clears all track selection overrides for all renderers. */ + /** + * Clears all track selection overrides for all renderers. + * + * @return This builder. + */ public final ParametersBuilder clearSelectionOverrides() { if (selectionOverrides.size() == 0) { // Nothing to clear. @@ -628,6 +711,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { exceedAudioConstraintsIfNecessary, allowAudioMixedMimeTypeAdaptiveness, allowAudioMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness, // Text preferredTextLanguage, selectUndeterminedTextLanguage, @@ -657,13 +741,27 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public static final class Parameters extends TrackSelectionParameters { - /** An instance with default values. */ - public static final Parameters DEFAULT = new Parameters(); + /** An instance with default values, except without any viewport constraints. */ + public static final Parameters DEFAULT_WITHOUT_VIEWPORT = new Parameters(); + + /** + * @deprecated This instance does not have viewport constraints configured for the primary + * display. Use {@link #getDefaults(Context)} instead. + */ + @Deprecated public static final Parameters DEFAULT = DEFAULT_WITHOUT_VIEWPORT; + + /** Returns an instance configured with default values. */ + public static Parameters getDefaults(Context context) { + return DEFAULT_WITHOUT_VIEWPORT + .buildUpon() + .setViewportSizeToPhysicalDisplaySize(context, /* viewportOrientationMayChange= */ true) + .build(); + } // Video /** - * Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no - * constraint). + * Maximum allowed video width in pixels. The default value is {@link Integer#MAX_VALUE} (i.e. + * no constraint). * *

    To constrain adaptive video track selections to be suitable for a given viewport (the * region of the display within which video will be played), use ({@link #viewportWidth}, {@link @@ -671,8 +769,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final int maxVideoWidth; /** - * Maximum allowed video height. The default value is {@link Integer#MAX_VALUE} (i.e. no - * constraint). + * Maximum allowed video height in pixels. The default value is {@link Integer#MAX_VALUE} (i.e. + * no constraint). * *

    To constrain adaptive video track selections to be suitable for a given viewport (the * region of the display within which video will be played), use ({@link #viewportWidth}, {@link @@ -680,12 +778,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final int maxVideoHeight; /** - * Maximum allowed video frame rate. The default value is {@link Integer#MAX_VALUE} (i.e. no - * constraint). + * Maximum allowed video frame rate in hertz. The default value is {@link Integer#MAX_VALUE} + * (i.e. no constraint). */ public final int maxVideoFrameRate; /** - * Maximum video bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint). + * Maximum allowed video bitrate in bits per second. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). */ public final int maxVideoBitrate; /** @@ -695,9 +794,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean exceedVideoConstraintsIfNecessary; /** - * Whether to allow adaptive video selections containing mixed mime types. Adaptations between - * different mime types may not be completely seamless, in which case {@link - * #allowVideoNonSeamlessAdaptiveness} also needs to be {@code true} for mixed mime type + * Whether to allow adaptive video selections containing mixed MIME types. Adaptations between + * different MIME types may not be completely seamless, in which case {@link + * #allowVideoNonSeamlessAdaptiveness} also needs to be {@code true} for mixed MIME type * selections to be made. The default value is {@code false}. */ public final boolean allowVideoMixedMimeTypeAdaptiveness; @@ -708,14 +807,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final boolean allowVideoNonSeamlessAdaptiveness; /** * Viewport width in pixels. Constrains video track selections for adaptive content so that only - * tracks suitable for the viewport are selected. The default value is {@link Integer#MAX_VALUE} - * (i.e. no constraint). + * tracks suitable for the viewport are selected. The default value is the physical width of the + * primary display, in pixels. */ public final int viewportWidth; /** * Viewport height in pixels. Constrains video track selections for adaptive content so that - * only tracks suitable for the viewport are selected. The default value is {@link - * Integer#MAX_VALUE} (i.e. no constraint). + * only tracks suitable for the viewport are selected. The default value is the physical height + * of the primary display, in pixels. */ public final int viewportHeight; /** @@ -731,7 +830,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final int maxAudioChannelCount; /** - * Maximum audio bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint). + * Maximum allowed audio bitrate in bits per second. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). */ public final int maxAudioBitrate; /** @@ -740,8 +840,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean exceedAudioConstraintsIfNecessary; /** - * Whether to allow adaptive audio selections containing mixed mime types. Adaptations between - * different mime types may not be completely seamless. The default value is {@code false}. + * Whether to allow adaptive audio selections containing mixed MIME types. Adaptations between + * different MIME types may not be completely seamless. The default value is {@code false}. */ public final boolean allowAudioMixedMimeTypeAdaptiveness; /** @@ -749,6 +849,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { * different sample rates may not be completely seamless. The default value is {@code false}. */ public final boolean allowAudioMixedSampleRateAdaptiveness; + /** + * Whether to allow adaptive audio selections containing mixed channel counts. Adaptations + * between different channel counts may not be completely seamless. The default value is {@code + * false}. + */ + public final boolean allowAudioMixedChannelCountAdaptiveness; // General /** @@ -809,6 +915,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /* exceedAudioConstraintsIfNecessary= */ true, /* allowAudioMixedMimeTypeAdaptiveness= */ false, /* allowAudioMixedSampleRateAdaptiveness= */ false, + /* allowAudioMixedChannelCountAdaptiveness= */ false, // Text TrackSelectionParameters.DEFAULT.preferredTextLanguage, TrackSelectionParameters.DEFAULT.selectUndeterminedTextLanguage, @@ -841,6 +948,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean exceedAudioConstraintsIfNecessary, boolean allowAudioMixedMimeTypeAdaptiveness, boolean allowAudioMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness, // Text @Nullable String preferredTextLanguage, boolean selectUndeterminedTextLanguage, @@ -875,6 +983,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary; this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness; this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness; + this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness; // General this.forceLowestBitrate = forceLowestBitrate; this.forceHighestSupportedBitrate = forceHighestSupportedBitrate; @@ -908,6 +1017,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.exceedAudioConstraintsIfNecessary = Util.readBoolean(in); this.allowAudioMixedMimeTypeAdaptiveness = Util.readBoolean(in); this.allowAudioMixedSampleRateAdaptiveness = Util.readBoolean(in); + this.allowAudioMixedChannelCountAdaptiveness = Util.readBoolean(in); // General this.forceLowestBitrate = Util.readBoolean(in); this.forceHighestSupportedBitrate = Util.readBoolean(in); @@ -989,6 +1099,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { && exceedAudioConstraintsIfNecessary == other.exceedAudioConstraintsIfNecessary && allowAudioMixedMimeTypeAdaptiveness == other.allowAudioMixedMimeTypeAdaptiveness && allowAudioMixedSampleRateAdaptiveness == other.allowAudioMixedSampleRateAdaptiveness + && allowAudioMixedChannelCountAdaptiveness + == other.allowAudioMixedChannelCountAdaptiveness // General && forceLowestBitrate == other.forceLowestBitrate && forceHighestSupportedBitrate == other.forceHighestSupportedBitrate @@ -1019,6 +1131,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + (exceedAudioConstraintsIfNecessary ? 1 : 0); result = 31 * result + (allowAudioMixedMimeTypeAdaptiveness ? 1 : 0); result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0); + result = 31 * result + (allowAudioMixedChannelCountAdaptiveness ? 1 : 0); // General result = 31 * result + (forceLowestBitrate ? 1 : 0); result = 31 * result + (forceHighestSupportedBitrate ? 1 : 0); @@ -1055,6 +1168,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { Util.writeBoolean(dest, exceedAudioConstraintsIfNecessary); Util.writeBoolean(dest, allowAudioMixedMimeTypeAdaptiveness); Util.writeBoolean(dest, allowAudioMixedSampleRateAdaptiveness); + Util.writeBoolean(dest, allowAudioMixedChannelCountAdaptiveness); // General Util.writeBoolean(dest, forceLowestBitrate); Util.writeBoolean(dest, forceHighestSupportedBitrate); @@ -1285,13 +1399,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean allowMultipleAdaptiveSelections; + /** @deprecated Use {@link #DefaultTrackSelector(Context)} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public DefaultTrackSelector() { this(new AdaptiveTrackSelection.Factory()); } /** - * @deprecated Use {@link #DefaultTrackSelector()} instead. Custom bandwidth meter should be - * directly passed to the player in {@link ExoPlayerFactory}. + * @deprecated Use {@link #DefaultTrackSelector(Context)} instead. The bandwidth meter should be + * passed directly to the player in {@link ExoPlayerFactory}. */ @Deprecated @SuppressWarnings("deprecation") @@ -1299,10 +1416,32 @@ public class DefaultTrackSelector extends MappingTrackSelector { this(new AdaptiveTrackSelection.Factory(bandwidthMeter)); } - /** @param trackSelectionFactory A factory for {@link TrackSelection}s. */ + /** @deprecated Use {@link #DefaultTrackSelector(Context, TrackSelection.Factory)}. */ + @Deprecated public DefaultTrackSelector(TrackSelection.Factory trackSelectionFactory) { + this(Parameters.DEFAULT_WITHOUT_VIEWPORT, trackSelectionFactory); + } + + /** @param context Any {@link Context}. */ + public DefaultTrackSelector(Context context) { + this(context, new AdaptiveTrackSelection.Factory()); + } + + /** + * @param context Any {@link Context}. + * @param trackSelectionFactory A factory for {@link TrackSelection}s. + */ + public DefaultTrackSelector(Context context, TrackSelection.Factory trackSelectionFactory) { + this(Parameters.getDefaults(context), trackSelectionFactory); + } + + /** + * @param parameters Initial {@link Parameters}. + * @param trackSelectionFactory A factory for {@link TrackSelection}s. + */ + public DefaultTrackSelector(Parameters parameters, TrackSelection.Factory trackSelectionFactory) { this.trackSelectionFactory = trackSelectionFactory; - parametersReference = new AtomicReference<>(Parameters.DEFAULT); + parametersReference = new AtomicReference<>(parameters); } /** @@ -1552,7 +1691,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - int selectedTextTrackScore = Integer.MIN_VALUE; + TextTrackScore selectedTextTrackScore = null; int selectedTextRendererIndex = C.INDEX_UNSET; for (int i = 0; i < rendererCount; i++) { int trackType = mappedTrackInfo.getRendererType(i); @@ -1562,13 +1701,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Already done. Do nothing. break; case C.TRACK_TYPE_TEXT: - Pair textSelection = + Pair textSelection = selectTextTrack( mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params, selectedAudioLanguage); - if (textSelection != null && textSelection.second > selectedTextTrackScore) { + if (textSelection != null + && (selectedTextTrackScore == null + || textSelection.second.compareTo(selectedTextTrackScore) > 0)) { if (selectedTextRendererIndex != C.INDEX_UNSET) { // We've already made a selection for another text renderer, but it had a lower score. // Clear the selection for that renderer. @@ -1934,8 +2075,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { getAdaptiveAudioTracks( selectedGroup, formatSupports[selectedGroupIndex], + params.maxAudioBitrate, params.allowAudioMixedMimeTypeAdaptiveness, - params.allowAudioMixedSampleRateAdaptiveness); + params.allowAudioMixedSampleRateAdaptiveness, + params.allowAudioMixedChannelCountAdaptiveness); if (adaptiveTracks.length > 0) { definition = new TrackSelection.Definition(selectedGroup, adaptiveTracks); } @@ -1951,8 +2094,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveAudioTracks( TrackGroup group, int[] formatSupport, + int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness) { + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { int selectedConfigurationTrackCount = 0; AudioConfigurationTuple selectedConfiguration = null; HashSet seenConfigurationTuples = new HashSet<>(); @@ -1967,8 +2112,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { group, formatSupport, configuration, + maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness); + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness); if (configurationCount > selectedConfigurationTrackCount) { selectedConfiguration = configuration; selectedConfigurationTrackCount = configurationCount; @@ -1977,15 +2124,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { } if (selectedConfigurationTrackCount > 1) { + Assertions.checkNotNull(selectedConfiguration); int[] adaptiveIndices = new int[selectedConfigurationTrackCount]; int index = 0; for (int i = 0; i < group.length; i++) { + Format format = group.getFormat(i); if (isSupportedAdaptiveAudioTrack( - group.getFormat(i), + format, formatSupport[i], - Assertions.checkNotNull(selectedConfiguration), + selectedConfiguration, + maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness)) { + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { adaptiveIndices[index++] = i; } } @@ -1998,16 +2149,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup group, int[] formatSupport, AudioConfigurationTuple configuration, + int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness) { + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { int count = 0; for (int i = 0; i < group.length; i++) { if (isSupportedAdaptiveAudioTrack( group.getFormat(i), formatSupport[i], configuration, + maxAudioBitrate, allowMixedMimeTypeAdaptiveness, - allowMixedSampleRateAdaptiveness)) { + allowMixedSampleRateAdaptiveness, + allowAudioMixedChannelCountAdaptiveness)) { count++; } } @@ -2018,11 +2173,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { Format format, int formatSupport, AudioConfigurationTuple configuration, + int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, - boolean allowMixedSampleRateAdaptiveness) { + boolean allowMixedSampleRateAdaptiveness, + boolean allowAudioMixedChannelCountAdaptiveness) { return isSupported(formatSupport, false) - && (format.channelCount != Format.NO_VALUE - && format.channelCount == configuration.channelCount) + && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxAudioBitrate) + && (allowAudioMixedChannelCountAdaptiveness + || (format.channelCount != Format.NO_VALUE + && format.channelCount == configuration.channelCount)) && (allowMixedMimeTypeAdaptiveness || (format.sampleMimeType != null && TextUtils.equals(format.sampleMimeType, configuration.mimeType))) @@ -2042,21 +2201,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { * track, indexed by track group index and track index (in that order). * @param params The selector's current constraint parameters. * @param selectedAudioLanguage The language of the selected audio track. May be null if the - * selected audio track declares no language or no audio track was selected. - * @return The {@link TrackSelection.Definition} and corresponding track score, or null if no - * selection was made. + * selected text track declares no language or no text track was selected. + * @return The {@link TrackSelection.Definition} and corresponding {@link TextTrackScore}, or null + * if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ @Nullable - protected Pair selectTextTrack( + protected Pair selectTextTrack( TrackGroupArray groups, int[][] formatSupport, Parameters params, @Nullable String selectedAudioLanguage) throws ExoPlaybackException { TrackGroup selectedGroup = null; - int selectedTrackIndex = 0; - int selectedTrackScore = 0; + int selectedTrackIndex = C.INDEX_UNSET; + TextTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; @@ -2064,39 +2223,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - int maskedSelectionFlags = - format.selectionFlags & ~params.disabledTextTrackSelectionFlags; - boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; - int trackScore; - int languageScore = getFormatLanguageScore(format, params.preferredTextLanguage); - boolean trackHasNoLanguage = formatHasNoLanguage(format); - if (languageScore > 0 || (params.selectUndeterminedTextLanguage && trackHasNoLanguage)) { - if (isDefault) { - trackScore = 11; - } else if (!isForced) { - // Prefer non-forced to forced if a preferred text language has been specified. Where - // both are provided the non-forced track will usually contain the forced subtitles as - // a subset. - trackScore = 7; - } else { - trackScore = 3; - } - trackScore += languageScore; - } else if (isDefault) { - trackScore = 2; - } else if (isForced - && (getFormatLanguageScore(format, selectedAudioLanguage) > 0 - || (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)))) { - trackScore = 1; - } else { - // Track should not be selected. - continue; - } - if (isSupported(trackFormatSupport[trackIndex], false)) { - trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; - } - if (trackScore > selectedTrackScore) { + TextTrackScore trackScore = + new TextTrackScore( + format, params, trackFormatSupport[trackIndex], selectedAudioLanguage); + if (trackScore.isWithinConstraints + && (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0)) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; @@ -2107,7 +2238,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { return selectedGroup == null ? null : Pair.create( - new TrackSelection.Definition(selectedGroup, selectedTrackIndex), selectedTrackScore); + new TrackSelection.Definition(selectedGroup, selectedTrackIndex), + Assertions.checkNotNull(selectedTrackScore)); } // General track selection methods. @@ -2277,19 +2409,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } - /** Equivalent to {@link #stringDefinesNoLanguage stringDefinesNoLanguage(format.language)}. */ - protected static boolean formatHasNoLanguage(Format format) { - return stringDefinesNoLanguage(format.language); - } - /** - * Returns whether the given string does not define a language. + * Normalizes the input string to null if it does not define a language, or returns it otherwise. * * @param language The string. - * @return Whether the given string does not define a language. + * @return The string, optionally normalized to null if it does not define a language. */ - protected static boolean stringDefinesNoLanguage(@Nullable String language) { - return TextUtils.isEmpty(language) || TextUtils.equals(language, C.LANGUAGE_UNDETERMINED); + @Nullable + protected static String normalizeUndeterminedLanguageToNull(@Nullable String language) { + return TextUtils.isEmpty(language) || TextUtils.equals(language, C.LANGUAGE_UNDETERMINED) + ? null + : language; } /** @@ -2297,27 +2427,35 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param format The {@link Format}. * @param language The language, or null. - * @return A score of 3 if the languages match fully, a score of 2 if the languages match partly, - * a score of 1 if the languages don't match but belong to the same main language, and a score - * of 0 if the languages don't match at all. + * @param allowUndeterminedFormatLanguage Whether matches with an empty or undetermined format + * language tag are allowed. + * @return A score of 4 if the languages match fully, a score of 3 if the languages match partly, + * a score of 2 if the languages don't match but belong to the same main language, a score of + * 1 if the format language is undetermined and such a match is allowed, and a score of 0 if + * the languages don't match at all. */ - protected static int getFormatLanguageScore(Format format, @Nullable String language) { - if (format.language == null || language == null) { - return 0; + protected static int getFormatLanguageScore( + Format format, @Nullable String language, boolean allowUndeterminedFormatLanguage) { + if (!TextUtils.isEmpty(language) && language.equals(format.language)) { + // Full literal match of non-empty languages, including matches of an explicit "und" query. + return 4; } - if (TextUtils.equals(format.language, language)) { + language = normalizeUndeterminedLanguageToNull(language); + String formatLanguage = normalizeUndeterminedLanguageToNull(format.language); + if (formatLanguage == null || language == null) { + // At least one of the languages is undetermined. + return allowUndeterminedFormatLanguage && formatLanguage == null ? 1 : 0; + } + if (formatLanguage.startsWith(language) || language.startsWith(formatLanguage)) { + // Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk") return 3; } - // Partial match where one language is a subset of the other (e.g. "zho-hans" and "zho-hans-hk") - if (format.language.startsWith(language) || language.startsWith(format.language)) { + String formatMainLanguage = Util.splitAtFirst(formatLanguage, "-")[0]; + String queryMainLanguage = Util.splitAtFirst(language, "-")[0]; + if (formatMainLanguage.equals(queryMainLanguage)) { + // Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca") return 2; } - // Partial match where only the main language tag is the same (e.g. "fra-fr" and "fra-ca") - if (format.language.length() >= 3 - && language.length() >= 3 - && format.language.substring(0, 3).equals(language.substring(0, 3))) { - return 1; - } return 0; } @@ -2390,9 +2528,25 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } + /** + * Compares two integers in a safe way avoiding potential overflow. + * + * @param first The first value. + * @param second The second value. + * @return A negative integer if the first value is less than the second. Zero if they are equal. + * A positive integer if the first value is greater than the second. + */ + private static int compareInts(int first, int second) { + return first > second ? 1 : (second > first ? -1 : 0); + } + /** Represents how well an audio track matches the selection {@link Parameters}. */ protected static final class AudioTrackScore implements Comparable { + /** + * Whether the provided format is within the parameter constraints. If {@code false}, the format + * should not be selected. + */ public final boolean isWithinConstraints; private final Parameters parameters; @@ -2408,7 +2562,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { public AudioTrackScore(Format format, Parameters parameters, int formatSupport) { this.parameters = parameters; isWithinRendererCapabilities = isSupported(formatSupport, false); - preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredAudioLanguage); + preferredLanguageScore = + getFormatLanguageScore( + format, + parameters.preferredAudioLanguage, + /* allowUndeterminedFormatLanguage= */ false); isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; channelCount = format.channelCount; sampleRate = format.sampleRate; @@ -2421,7 +2579,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { int bestMatchIndex = Integer.MAX_VALUE; int bestMatchScore = 0; for (int i = 0; i < localeLanguages.length; i++) { - int score = getFormatLanguageScore(format, localeLanguages[i]); + int score = + getFormatLanguageScore( + format, localeLanguages[i], /* allowUndeterminedFormatLanguage= */ false); if (score > 0) { bestMatchIndex = i; bestMatchScore = score; @@ -2440,7 +2600,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * negative integer if this score is worse than the other. */ @Override - public int compareTo(@NonNull AudioTrackScore other) { + public int compareTo(AudioTrackScore other) { if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { return this.isWithinRendererCapabilities ? 1 : -1; } @@ -2478,18 +2638,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - /** - * Compares two integers in a safe way and avoiding potential overflow. - * - * @param first The first value. - * @param second The second value. - * @return A negative integer if the first value is less than the second. Zero if they are equal. - * A positive integer if the first value is greater than the second. - */ - private static int compareInts(int first, int second) { - return first > second ? 1 : (second > first ? -1 : 0); - } - private static final class AudioConfigurationTuple { public final int channelCount; @@ -2525,4 +2673,70 @@ public class DefaultTrackSelector extends MappingTrackSelector { } + /** Represents how well a text track matches the selection {@link Parameters}. */ + protected static final class TextTrackScore implements Comparable { + + /** + * Whether the provided format is within the parameter constraints. If {@code false}, the format + * should not be selected. + */ + public final boolean isWithinConstraints; + + private final boolean isWithinRendererCapabilities; + private final boolean isDefault; + private final boolean hasPreferredIsForcedFlag; + private final int preferredLanguageScore; + private final int selectedAudioLanguageScore; + + public TextTrackScore( + Format format, + Parameters parameters, + int trackFormatSupport, + @Nullable String selectedAudioLanguage) { + isWithinRendererCapabilities = + isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false); + int maskedSelectionFlags = + format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; + isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; + preferredLanguageScore = + getFormatLanguageScore( + format, parameters.preferredTextLanguage, parameters.selectUndeterminedTextLanguage); + // Prefer non-forced to forced if a preferred text language has been matched. Where both are + // provided the non-forced track will usually contain the forced subtitles as a subset. + // Otherwise, prefer a forced track. + hasPreferredIsForcedFlag = + (preferredLanguageScore > 0 && !isForced) || (preferredLanguageScore == 0 && isForced); + boolean selectedAudioLanguageUndetermined = + normalizeUndeterminedLanguageToNull(selectedAudioLanguage) == null; + selectedAudioLanguageScore = + getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined); + isWithinConstraints = + preferredLanguageScore > 0 || isDefault || (isForced && selectedAudioLanguageScore > 0); + } + + /** + * Compares this score with another. + * + * @param other The other score to compare to. + * @return A positive integer if this score is better than the other. Zero if they are equal. A + * negative integer if this score is worse than the other. + */ + @Override + public int compareTo(TextTrackScore other) { + if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { + return this.isWithinRendererCapabilities ? 1 : -1; + } + if (this.preferredLanguageScore != other.preferredLanguageScore) { + return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); + } + if (this.isDefault != other.isDefault) { + return this.isDefault ? 1 : -1; + } + if (this.hasPreferredIsForcedFlag != other.hasPreferredIsForcedFlag) { + return this.hasPreferredIsForcedFlag ? 1 : -1; + } + return compareInts(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java index 3bdaeeeafb..fefad00cbd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java @@ -39,7 +39,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { public static final class Factory implements TrackSelection.Factory { private final int reason; - private final @Nullable Object data; + @Nullable private final Object data; public Factory() { this.reason = C.SELECTION_REASON_UNKNOWN; @@ -66,7 +66,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { } private final int reason; - private final @Nullable Object data; + @Nullable private final Object data; /** * @param group The {@link TrackGroup}. Must not be null. @@ -109,7 +109,8 @@ public final class FixedTrackSelection extends BaseTrackSelection { } @Override - public @Nullable Object getSelectionData() { + @Nullable + public Object getSelectionData() { return data; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index dfb19e3bca..5587af9cbf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -312,7 +312,7 @@ public abstract class MappingTrackSelector extends TrackSelector { } - private @Nullable MappedTrackInfo currentMappedTrackInfo; + @Nullable private MappedTrackInfo currentMappedTrackInfo; /** * Returns the mapping information for the currently active track selection, or null if no diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java index 8053212969..f35e7ec755 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java @@ -135,7 +135,8 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override - public @Nullable Object getSelectionData() { + @Nullable + public Object getSelectionData() { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java index bc905ace4b..fc20e863ba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java @@ -42,7 +42,8 @@ public final class TrackSelectionArray { * @param index The index of the selection. * @return The selection. */ - public @Nullable TrackSelection get(int index) { + @Nullable + public TrackSelection get(int index) { return trackSelections[index]; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java index 66a4707496..c406f262d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -31,9 +31,7 @@ public class TrackSelectionParameters implements Parcelable { */ public static class Builder { - // Audio @Nullable /* package */ String preferredAudioLanguage; - // Text @Nullable /* package */ String preferredTextLanguage; /* package */ boolean selectUndeterminedTextLanguage; @C.SelectionFlags /* package */ int disabledTextTrackSelectionFlags; @@ -48,18 +46,17 @@ public class TrackSelectionParameters implements Parcelable { * the builder are obtained. */ /* package */ Builder(TrackSelectionParameters initialValues) { - // Audio preferredAudioLanguage = initialValues.preferredAudioLanguage; - // Text preferredTextLanguage = initialValues.preferredTextLanguage; selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags; } /** - * See {@link TrackSelectionParameters#preferredAudioLanguage}. + * Sets the preferred language for audio and forced text tracks. * - * @param preferredAudioLanguage Preferred audio language as an IETF BCP 47 conformant tag. + * @param preferredAudioLanguage Preferred audio language as an IETF BCP 47 conformant tag, or + * {@code null} to select the default track, or the first track if there's no default. * @return This builder. */ public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) { @@ -67,12 +64,11 @@ public class TrackSelectionParameters implements Parcelable { return this; } - // Text - /** - * See {@link TrackSelectionParameters#preferredTextLanguage}. + * Sets the preferred language for text tracks. * - * @param preferredTextLanguage Preferred text language as an IETF BCP 47 conformant tag. + * @param preferredTextLanguage Preferred text language as an IETF BCP 47 conformant tag, or + * {@code null} to select the default track if there is one, or no track otherwise. * @return This builder. */ public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) { @@ -81,8 +77,12 @@ public class TrackSelectionParameters implements Parcelable { } /** - * See {@link TrackSelectionParameters#selectUndeterminedTextLanguage}. + * Sets whether a text track with undetermined language should be selected if no track with + * {@link #setPreferredTextLanguage(String)} is available, or if the preferred language is + * unset. * + * @param selectUndeterminedTextLanguage Whether a text track with undetermined language should + * be selected if no preferred language track is available. * @return This builder. */ public Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { @@ -91,8 +91,10 @@ public class TrackSelectionParameters implements Parcelable { } /** - * See {@link TrackSelectionParameters#disabledTextTrackSelectionFlags}. + * Sets a bitmask of selection flags that are disabled for text track selections. * + * @param disabledTextTrackSelectionFlags A bitmask of {@link C.SelectionFlags} that are + * disabled for text track selections. * @return This builder. */ public Builder setDisabledTextTrackSelectionFlags( @@ -117,15 +119,14 @@ public class TrackSelectionParameters implements Parcelable { public static final TrackSelectionParameters DEFAULT = new TrackSelectionParameters(); /** - * The preferred language for audio and forced text tracks, as an ISO 639-2/T tag. {@code null} - * selects the default track, or the first track if there's no default. The default value is - * {@code null}. + * The preferred language for audio and forced text tracks as an IETF BCP 47 conformant tag. + * {@code null} selects the default track, or the first track if there's no default. The default + * value is {@code null}. */ @Nullable public final String preferredAudioLanguage; - // Text /** - * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the default - * track if there is one, or no track otherwise. The default value is {@code null}. + * The preferred language for text tracks as an IETF BCP 47 conformant tag. {@code null} selects + * the default track if there is one, or no track otherwise. The default value is {@code null}. */ @Nullable public final String preferredTextLanguage; /** @@ -163,9 +164,7 @@ public class TrackSelectionParameters implements Parcelable { } /* package */ TrackSelectionParameters(Parcel in) { - // Audio this.preferredAudioLanguage = in.readString(); - // Text this.preferredTextLanguage = in.readString(); this.selectUndeterminedTextLanguage = Util.readBoolean(in); this.disabledTextTrackSelectionFlags = in.readInt(); @@ -187,7 +186,6 @@ public class TrackSelectionParameters implements Parcelable { } TrackSelectionParameters other = (TrackSelectionParameters) obj; return TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) - // Text && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage) && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags; @@ -196,9 +194,7 @@ public class TrackSelectionParameters implements Parcelable { @Override public int hashCode() { int result = 1; - // Audio result = 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode()); - // Text result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode()); result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0); result = 31 * result + disabledTextTrackSelectionFlags; @@ -214,9 +210,7 @@ public class TrackSelectionParameters implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - // Audio dest.writeString(preferredAudioLanguage); - // Text dest.writeString(preferredTextLanguage); Util.writeBoolean(dest, selectUndeterminedTextLanguage); dest.writeInt(disabledTextTrackSelectionFlags); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java index f2fbd89118..fb74bd9d54 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -98,8 +98,8 @@ public abstract class TrackSelector { } - private @Nullable InvalidationListener listener; - private @Nullable BandwidthMeter bandwidthMeter; + @Nullable private InvalidationListener listener; + @Nullable private BandwidthMeter bandwidthMeter; /** * Called by the player to initialize the selector. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index fc723134f7..9228f3af62 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -85,8 +85,8 @@ public final class TrackSelectorResult { /** * Returns whether this result is equivalent to {@code other} for the renderer at the given index. - * The results are equivalent if they have equal renderersEnabled array, track selections, and - * configurations for the renderer. + * The results are equivalent if they have equal track selections and configurations for the + * renderer. * * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} * will be returned. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java index 9224e14d4a..3c92b039cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java @@ -15,11 +15,14 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.Context; import android.content.res.AssetManager; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; @@ -40,8 +43,8 @@ public final class AssetDataSource extends BaseDataSource { private final AssetManager assetManager; - private @Nullable Uri uri; - private @Nullable InputStream inputStream; + @Nullable private Uri uri; + @Nullable private InputStream inputStream; private long bytesRemaining; private boolean opened; @@ -51,25 +54,11 @@ public final class AssetDataSource extends BaseDataSource { this.assetManager = context.getAssets(); } - /** - * @param context A context. - * @param listener An optional listener. - * @deprecated Use {@link #AssetDataSource(Context)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public AssetDataSource(Context context, @Nullable TransferListener listener) { - this(context); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws AssetDataSourceException { try { uri = dataSpec.uri; - String path = uri.getPath(); + String path = Assertions.checkNotNull(uri.getPath()); if (path.startsWith("/android_asset/")) { path = path.substring(15); } else if (path.startsWith("/")) { @@ -115,7 +104,7 @@ public final class AssetDataSource extends BaseDataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new AssetDataSourceException(e); } @@ -135,7 +124,8 @@ public final class AssetDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java index 21f2d5993a..80687db31f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java @@ -33,7 +33,7 @@ public abstract class BaseDataSource implements DataSource { private final ArrayList listeners; private int listenerCount; - private @Nullable DataSpec dataSpec; + @Nullable private DataSpec dataSpec; /** * Creates base data source. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java index 4017c1f028..2ba6ab4c69 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java @@ -15,20 +15,24 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.ByteArrayOutputStream; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link DataSink} for writing to a byte array. */ public final class ByteArrayDataSink implements DataSink { - private ByteArrayOutputStream stream; + private @MonotonicNonNull ByteArrayOutputStream stream; @Override - public void open(DataSpec dataSpec) throws IOException { + public void open(DataSpec dataSpec) { if (dataSpec.length == C.LENGTH_UNSET) { stream = new ByteArrayOutputStream(); } else { @@ -39,18 +43,19 @@ public final class ByteArrayDataSink implements DataSink { @Override public void close() throws IOException { - stream.close(); + castNonNull(stream).close(); } @Override - public void write(byte[] buffer, int offset, int length) throws IOException { - stream.write(buffer, offset, length); + public void write(byte[] buffer, int offset, int length) { + castNonNull(stream).write(buffer, offset, length); } /** * Returns the data written to the sink since the last call to {@link #open(DataSpec)}, or null if * {@link #open(DataSpec)} has never been called. */ + @Nullable public byte[] getData() { return stream == null ? null : stream.toByteArray(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java index c450896676..ed5ba9064b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java @@ -26,7 +26,7 @@ public final class ByteArrayDataSource extends BaseDataSource { private final byte[] data; - private @Nullable Uri uri; + @Nullable private Uri uri; private int readPosition; private int bytesRemaining; private boolean opened; @@ -58,7 +58,7 @@ public final class ByteArrayDataSource extends BaseDataSource { } @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { + public int read(byte[] buffer, int offset, int readLength) { if (readLength == 0) { return 0; } else if (bytesRemaining == 0) { @@ -74,12 +74,13 @@ public final class ByteArrayDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } @Override - public void close() throws IOException { + public void close() { if (opened) { opened = false; transferEnded(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index c723d3f1ca..baaa677127 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; @@ -43,9 +45,9 @@ public final class ContentDataSource extends BaseDataSource { private final ContentResolver resolver; - private @Nullable Uri uri; - private @Nullable AssetFileDescriptor assetFileDescriptor; - private @Nullable FileInputStream inputStream; + @Nullable private Uri uri; + @Nullable private AssetFileDescriptor assetFileDescriptor; + @Nullable private FileInputStream inputStream; private long bytesRemaining; private boolean opened; @@ -57,30 +59,21 @@ public final class ContentDataSource extends BaseDataSource { this.resolver = context.getContentResolver(); } - /** - * @param context A context. - * @param listener An optional listener. - * @deprecated Use {@link #ContentDataSource(Context)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public ContentDataSource(Context context, @Nullable TransferListener listener) { - this(context); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws ContentDataSourceException { try { - uri = dataSpec.uri; + Uri uri = dataSpec.uri; + this.uri = uri; + transferInitializing(dataSpec); - assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); + AssetFileDescriptor assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); + this.assetFileDescriptor = assetFileDescriptor; if (assetFileDescriptor == null) { throw new FileNotFoundException("Could not open file descriptor for: " + uri); } - inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + this.inputStream = inputStream; + long assetStartOffset = assetFileDescriptor.getStartOffset(); long skipped = inputStream.skip(assetStartOffset + dataSpec.position) - assetStartOffset; if (skipped != dataSpec.position) { @@ -124,7 +117,7 @@ public final class ContentDataSource extends BaseDataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new ContentDataSourceException(e); } @@ -144,7 +137,8 @@ public final class ContentDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java index de4a75d607..94a6e21c86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import android.util.Base64; @@ -29,9 +31,10 @@ public final class DataSchemeDataSource extends BaseDataSource { public static final String SCHEME_DATA = "data"; - private @Nullable DataSpec dataSpec; - private int bytesRead; - private @Nullable byte[] data; + @Nullable private DataSpec dataSpec; + @Nullable private byte[] data; + private int endPosition; + private int readPosition; public DataSchemeDataSource() { super(/* isNetwork= */ false); @@ -41,6 +44,7 @@ public final class DataSchemeDataSource extends BaseDataSource { public long open(DataSpec dataSpec) throws IOException { transferInitializing(dataSpec); this.dataSpec = dataSpec; + readPosition = (int) dataSpec.position; Uri uri = dataSpec.uri; String scheme = uri.getScheme(); if (!SCHEME_DATA.equals(scheme)) { @@ -61,8 +65,14 @@ public final class DataSchemeDataSource extends BaseDataSource { // TODO: Add support for other charsets. data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME)); } + endPosition = + dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length; + if (endPosition > data.length || readPosition > endPosition) { + data = null; + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); + } transferStarted(dataSpec); - return data.length; + return (long) endPosition - readPosition; } @Override @@ -70,29 +80,29 @@ public final class DataSchemeDataSource extends BaseDataSource { if (readLength == 0) { return 0; } - int remainingBytes = data.length - bytesRead; + int remainingBytes = endPosition - readPosition; if (remainingBytes == 0) { return C.RESULT_END_OF_INPUT; } readLength = Math.min(readLength, remainingBytes); - System.arraycopy(data, bytesRead, buffer, offset, readLength); - bytesRead += readLength; + System.arraycopy(castNonNull(data), readPosition, buffer, offset, readLength); + readPosition += readLength; bytesTransferred(readLength); return readLength; } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSpec != null ? dataSpec.uri : null; } @Override - public void close() throws IOException { + public void close() { if (data != null) { data = null; transferEnded(); } dataSpec = null; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index a98f773c9d..c2007b19a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -95,12 +95,10 @@ public final class DataSpec { public final @HttpMethod int httpMethod; /** - * The HTTP body, null otherwise. If the body is non-null, then httpBody.length will be non-zero. + * The HTTP request body, null otherwise. If the body is non-null, then httpBody.length will be + * non-zero. */ - public final @Nullable byte[] httpBody; - - /** @deprecated Use {@link #httpBody} instead. */ - @Deprecated public final @Nullable byte[] postBody; + @Nullable public final byte[] httpBody; /** * The absolute position of the data in the full stream. @@ -121,7 +119,7 @@ public final class DataSpec { * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the * data spec is not intended to be used in conjunction with a cache. */ - public final @Nullable String key; + @Nullable public final String key; /** Request {@link Flags flags}. */ public final @Flags int flags; @@ -251,7 +249,6 @@ public final class DataSpec { this.uri = uri; this.httpMethod = httpMethod; this.httpBody = (httpBody != null && httpBody.length != 0) ? httpBody : null; - this.postBody = this.httpBody; this.absoluteStreamPosition = absoluteStreamPosition; this.position = position; this.length = length; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index b2333516a8..4145d9a1c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -413,7 +413,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList */ private static class ConnectivityActionReceiver extends BroadcastReceiver { - @MonotonicNonNull private static ConnectivityActionReceiver staticInstance; + private static @MonotonicNonNull ConnectivityActionReceiver staticInstance; private final Handler mainHandler; private final ArrayList> bandwidthMeters; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index 8b4107850c..dec035c12e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -42,10 +42,11 @@ import java.util.Map; *

  • rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an * explicit dependency on ExoPlayer's RTMP extension. *
  • data: For parsing data inlined in the URI as defined in RFC 2397. + *
  • udp: For fetching data over UDP (e.g. udp://something.com/media). *
  • http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), - * if constructed using {@link #DefaultDataSource(Context, TransferListener, String, - * boolean)}, or any other schemes supported by a base data source if constructed using {@link - * #DefaultDataSource(Context, TransferListener, DataSource)}. + * if constructed using {@link #DefaultDataSource(Context, String, boolean)}, or any other + * schemes supported by a base data source if constructed using {@link + * #DefaultDataSource(Context, DataSource)}. *
*/ public final class DefaultDataSource implements DataSource { @@ -55,6 +56,7 @@ public final class DefaultDataSource implements DataSource { private static final String SCHEME_ASSET = "asset"; private static final String SCHEME_CONTENT = "content"; private static final String SCHEME_RTMP = "rtmp"; + private static final String SCHEME_UDP = "udp"; private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME; private final Context context; @@ -62,14 +64,15 @@ public final class DefaultDataSource implements DataSource { private final DataSource baseDataSource; // Lazily initialized. - private @Nullable DataSource fileDataSource; - private @Nullable DataSource assetDataSource; - private @Nullable DataSource contentDataSource; - private @Nullable DataSource rtmpDataSource; - private @Nullable DataSource dataSchemeDataSource; - private @Nullable DataSource rawResourceDataSource; + @Nullable private DataSource fileDataSource; + @Nullable private DataSource assetDataSource; + @Nullable private DataSource contentDataSource; + @Nullable private DataSource rtmpDataSource; + @Nullable private DataSource udpDataSource; + @Nullable private DataSource dataSchemeDataSource; + @Nullable private DataSource rawResourceDataSource; - private @Nullable DataSource dataSource; + @Nullable private DataSource dataSource; /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. @@ -131,85 +134,6 @@ public final class DefaultDataSource implements DataSource { transferListeners = new ArrayList<>(); } - /** - * Constructs a new instance, optionally configured to follow cross-protocol redirects. - * - * @param context A context. - * @param listener An optional listener. - * @param userAgent The User-Agent to use when requesting remote data. - * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP - * to HTTPS and vice versa) are enabled when fetching remote data. - * @deprecated Use {@link #DefaultDataSource(Context, String, boolean)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultDataSource( - Context context, - @Nullable TransferListener listener, - String userAgent, - boolean allowCrossProtocolRedirects) { - this(context, listener, userAgent, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, - DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects); - } - - /** - * Constructs a new instance, optionally configured to follow cross-protocol redirects. - * - * @param context A context. - * @param listener An optional listener. - * @param userAgent The User-Agent to use when requesting remote data. - * @param connectTimeoutMillis The connection timeout that should be used when requesting remote - * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout. - * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in - * milliseconds. A timeout of zero is interpreted as an infinite timeout. - * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP - * to HTTPS and vice versa) are enabled when fetching remote data. - * @deprecated Use {@link #DefaultDataSource(Context, String, int, int, boolean)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultDataSource( - Context context, - @Nullable TransferListener listener, - String userAgent, - int connectTimeoutMillis, - int readTimeoutMillis, - boolean allowCrossProtocolRedirects) { - this( - context, - listener, - new DefaultHttpDataSource( - userAgent, - /* contentTypePredicate= */ null, - listener, - connectTimeoutMillis, - readTimeoutMillis, - allowCrossProtocolRedirects, - /* defaultRequestProperties= */ null)); - } - - /** - * Constructs a new instance that delegates to a provided {@link DataSource} for URI schemes other - * than file, asset and content. - * - * @param context A context. - * @param listener An optional listener. - * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and - * content. This {@link DataSource} should normally support at least http(s). - * @deprecated Use {@link #DefaultDataSource(Context, DataSource)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public DefaultDataSource( - Context context, @Nullable TransferListener listener, DataSource baseDataSource) { - this(context, baseDataSource); - if (listener != null) { - transferListeners.add(listener); - } - } - @Override public void addTransferListener(TransferListener transferListener) { baseDataSource.addTransferListener(transferListener); @@ -218,6 +142,7 @@ public final class DefaultDataSource implements DataSource { maybeAddListenerToDataSource(assetDataSource, transferListener); maybeAddListenerToDataSource(contentDataSource, transferListener); maybeAddListenerToDataSource(rtmpDataSource, transferListener); + maybeAddListenerToDataSource(udpDataSource, transferListener); maybeAddListenerToDataSource(dataSchemeDataSource, transferListener); maybeAddListenerToDataSource(rawResourceDataSource, transferListener); } @@ -240,6 +165,8 @@ public final class DefaultDataSource implements DataSource { dataSource = getContentDataSource(); } else if (SCHEME_RTMP.equals(scheme)) { dataSource = getRtmpDataSource(); + } else if (SCHEME_UDP.equals(scheme)) { + dataSource = getUdpDataSource(); } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) { dataSource = getDataSchemeDataSource(); } else if (SCHEME_RAW.equals(scheme)) { @@ -257,7 +184,8 @@ public final class DefaultDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSource == null ? null : dataSource.getUri(); } @@ -277,6 +205,14 @@ public final class DefaultDataSource implements DataSource { } } + private DataSource getUdpDataSource() { + if (udpDataSource == null) { + udpDataSource = new UdpDataSource(); + addListenersToDataSource(udpDataSource); + } + return udpDataSource; + } + private DataSource getFileDataSource() { if (fileDataSource == null) { fileDataSource = new FileDataSource(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java index 9639b4ede1..6b1131a3bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.upstream.DataSource.Factory; public final class DefaultDataSourceFactory implements Factory { private final Context context; - private final @Nullable TransferListener listener; + @Nullable private final TransferListener listener; private final DataSource.Factory baseDataSourceFactory; /** @@ -51,7 +51,7 @@ public final class DefaultDataSourceFactory implements Factory { * @param context A context. * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource} * for {@link DefaultDataSource}. - * @see DefaultDataSource#DefaultDataSource(Context, TransferListener, DataSource) + * @see DefaultDataSource#DefaultDataSource(Context, DataSource) */ public DefaultDataSourceFactory(Context context, DataSource.Factory baseDataSourceFactory) { this(context, /* listener= */ null, baseDataSourceFactory); @@ -62,7 +62,7 @@ public final class DefaultDataSourceFactory implements Factory { * @param listener An optional listener. * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource} * for {@link DefaultDataSource}. - * @see DefaultDataSource#DefaultDataSource(Context, TransferListener, DataSource) + * @see DefaultDataSource#DefaultDataSource(Context, DataSource) */ public DefaultDataSourceFactory( Context context, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 66036b7a84..3ee1ef7564 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -41,14 +41,15 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; /** * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}. * *

By default this implementation will not follow cross-protocol redirects (i.e. redirects from * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the {@link - * #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean, - * RequestProperties)} constructor and passing {@code true} as the second last argument. + * #DefaultHttpDataSource(String, Predicate, int, int, boolean, RequestProperties)} constructor and + * passing {@code true} as the second last argument. */ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource { @@ -74,13 +75,13 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private final int connectTimeoutMillis; private final int readTimeoutMillis; private final String userAgent; - private final @Nullable Predicate contentTypePredicate; - private final @Nullable RequestProperties defaultRequestProperties; + @Nullable private final RequestProperties defaultRequestProperties; private final RequestProperties requestProperties; - private @Nullable DataSpec dataSpec; - private @Nullable HttpURLConnection connection; - private @Nullable InputStream inputStream; + @Nullable private Predicate contentTypePredicate; + @Nullable private DataSpec dataSpec; + @Nullable private HttpURLConnection connection; + @Nullable private InputStream inputStream; private boolean opened; private long bytesToSkip; @@ -91,7 +92,50 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou /** @param userAgent The User-Agent string that should be used. */ public DefaultHttpDataSource(String userAgent) { - this(userAgent, /* contentTypePredicate= */ null); + this(userAgent, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is + * interpreted as an infinite timeout. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. + */ + public DefaultHttpDataSource(String userAgent, int connectTimeoutMillis, int readTimeoutMillis) { + this( + userAgent, + connectTimeoutMillis, + readTimeoutMillis, + /* allowCrossProtocolRedirects= */ false, + /* defaultRequestProperties= */ null); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is + * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the + * default value. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as + * an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. + * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP + * to HTTPS and vice versa) are enabled. + * @param defaultRequestProperties The default request properties to be sent to the server as HTTP + * headers or {@code null} if not required. + */ + public DefaultHttpDataSource( + String userAgent, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects, + @Nullable RequestProperties defaultRequestProperties) { + super(/* isNetwork= */ true); + this.userAgent = Assertions.checkNotEmpty(userAgent); + this.requestProperties = new RequestProperties(); + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutMillis = readTimeoutMillis; + this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; + this.defaultRequestProperties = defaultRequestProperties; } /** @@ -99,7 +143,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link * #open(DataSpec)}. + * @deprecated Use {@link #DefaultHttpDataSource(String)} and {@link + * #setContentTypePredicate(Predicate)}. */ + @Deprecated public DefaultHttpDataSource(String userAgent, @Nullable Predicate contentTypePredicate) { this( userAgent, @@ -117,7 +164,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * interpreted as an infinite timeout. * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as * an infinite timeout. + * @deprecated Use {@link #DefaultHttpDataSource(String, int, int)} and {@link + * #setContentTypePredicate(Predicate)}. */ + @Deprecated public DefaultHttpDataSource( String userAgent, @Nullable Predicate contentTypePredicate, @@ -146,7 +196,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * to HTTPS and vice versa) are enabled. * @param defaultRequestProperties The default request properties to be sent to the server as HTTP * headers or {@code null} if not required. + * @deprecated Use {@link #DefaultHttpDataSource(String, int, int, boolean, RequestProperties)} + * and {@link #setContentTypePredicate(Predicate)}. */ + @Deprecated public DefaultHttpDataSource( String userAgent, @Nullable Predicate contentTypePredicate, @@ -165,90 +218,19 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } /** - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link - * #open(DataSpec)}. - * @param listener An optional listener. - * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate)} and {@link - * #addTransferListener(TransferListener)}. + * 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. */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultHttpDataSource( - String userAgent, - @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener) { - this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, - DEFAULT_READ_TIMEOUT_MILLIS); - } - - /** - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link - * #open(DataSpec)}. - * @param listener An optional listener. - * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is - * interpreted as an infinite timeout. - * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as - * an infinite timeout. - * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate, int, int)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultHttpDataSource( - String userAgent, - @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener, - int connectTimeoutMillis, - int readTimeoutMillis) { - this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false, - null); - } - - /** - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the - * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link - * #open(DataSpec)}. - * @param listener An optional listener. - * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is - * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the - * default value. - * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as - * an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. - * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP - * to HTTPS and vice versa) are enabled. - * @param defaultRequestProperties The default request properties to be sent to the server as HTTP - * headers or {@code null} if not required. - * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate, int, int, boolean, - * RequestProperties)} and {@link #addTransferListener(TransferListener)}. - */ - @Deprecated - public DefaultHttpDataSource( - String userAgent, - @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener, - int connectTimeoutMillis, - int readTimeoutMillis, - boolean allowCrossProtocolRedirects, - @Nullable RequestProperties defaultRequestProperties) { - this( - userAgent, - contentTypePredicate, - connectTimeoutMillis, - readTimeoutMillis, - allowCrossProtocolRedirects, - defaultRequestProperties); - if (listener != null) { - addTransferListener(listener); - } + public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) { + this.contentTypePredicate = contentTypePredicate; } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return connection == null ? null : Uri.parse(connection.getURL().toString()); } @@ -324,7 +306,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; // Determine the length of the data to be read, after skipping. - if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) { + boolean isCompressed = isCompressed(connection); + if (!isCompressed) { if (dataSpec.length != C.LENGTH_UNSET) { bytesToRead = dataSpec.length; } else { @@ -334,14 +317,16 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } } else { // Gzip is enabled. If the server opts to use gzip then the content length in the response - // will be that of the compressed data, which isn't what we want. Furthermore, there isn't a - // reliable way to determine whether the gzip was used or not. Always use the dataSpec length - // in this case. + // will be that of the compressed data, which isn't what we want. Always use the dataSpec + // length in this case. bytesToRead = dataSpec.length; } try { inputStream = connection.getInputStream(); + if (isCompressed) { + inputStream = new GZIPInputStream(inputStream); + } } catch (IOException e) { closeConnectionQuietly(); throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN); @@ -535,9 +520,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou connection.setRequestProperty("Range", rangeRequest); } connection.setRequestProperty("User-Agent", userAgent); - if (!allowGzip) { - connection.setRequestProperty("Accept-Encoding", "identity"); - } + connection.setRequestProperty("Accept-Encoding", allowGzip ? "gzip" : "identity"); if (allowIcyMetadata) { connection.setRequestProperty( IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME, @@ -766,4 +749,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } } + private static boolean isCompressed(HttpURLConnection connection) { + String contentEncoding = connection.getHeaderField("Content-Encoding"); + return "gzip".equalsIgnoreCase(contentEncoding); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java index 371343857f..f5d7dbd24c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.util.Assertions; public final class DefaultHttpDataSourceFactory extends BaseFactory { private final String userAgent; - private final @Nullable TransferListener listener; + @Nullable private final TransferListener listener; private final int connectTimeoutMillis; private final int readTimeoutMillis; private final boolean allowCrossProtocolRedirects; @@ -107,7 +107,6 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { DefaultHttpDataSource dataSource = new DefaultHttpDataSource( userAgent, - /* contentTypePredicate= */ null, connectTimeoutMillis, readTimeoutMillis, allowCrossProtocolRedirects, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java index 026bc0b9c7..4124a2531f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java @@ -42,17 +42,18 @@ public final class DummyDataSource implements DataSource { } @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { + public int read(byte[] buffer, int offset, int readLength) { throw new UnsupportedOperationException(); } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return null; } @Override - public void close() throws IOException { + public void close() { // do nothing. } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java index 3cfdc4812b..e329dc722e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java @@ -15,9 +15,12 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.IOException; import java.io.RandomAccessFile; @@ -36,8 +39,8 @@ public final class FileDataSource extends BaseDataSource { } - private @Nullable RandomAccessFile file; - private @Nullable Uri uri; + @Nullable private RandomAccessFile file; + @Nullable private Uri uri; private long bytesRemaining; private boolean opened; @@ -45,24 +48,16 @@ public final class FileDataSource extends BaseDataSource { super(/* isNetwork= */ false); } - /** - * @param listener An optional listener. - * @deprecated Use {@link #FileDataSource()} and {@link #addTransferListener(TransferListener)} - */ - @Deprecated - public FileDataSource(@Nullable TransferListener listener) { - this(); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws FileDataSourceException { try { - uri = dataSpec.uri; + Uri uri = dataSpec.uri; + this.uri = uri; + transferInitializing(dataSpec); - file = new RandomAccessFile(dataSpec.uri.getPath(), "r"); + RandomAccessFile file = new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), "r"); + this.file = file; + file.seek(dataSpec.position); bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position : dataSpec.length; @@ -88,7 +83,8 @@ public final class FileDataSource extends BaseDataSource { } else { int bytesRead; try { - bytesRead = file.read(buffer, offset, (int) Math.min(bytesRemaining, readLength)); + bytesRead = + castNonNull(file).read(buffer, offset, (int) Math.min(bytesRemaining, readLength)); } catch (IOException e) { throw new FileDataSourceException(e); } @@ -103,7 +99,8 @@ public final class FileDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java index 3a47df7654..0b4de1b43e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java @@ -22,7 +22,7 @@ import androidx.annotation.Nullable; */ public final class FileDataSourceFactory implements DataSource.Factory { - private final @Nullable TransferListener listener; + @Nullable private final TransferListener listener; public FileDataSourceFactory() { this(null); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 07155ee2bc..17fb4ad7a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -183,18 +183,21 @@ public interface HttpDataSource extends DataSource { return defaultRequestProperties; } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void setDefaultRequestProperty(String name, String value) { defaultRequestProperties.set(name, value); } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void clearDefaultRequestProperty(String name) { defaultRequestProperties.remove(name); } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void clearAllDefaultRequestProperties() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 878c40dc9e..b5a13f3b80 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -311,7 +311,7 @@ public final class Loader implements LoaderErrorThrower { private final T loadable; private final long startTimeMs; - private @Nullable Loader.Callback callback; + @Nullable private Loader.Callback callback; private IOException currentError; private int errorCount; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java index 62e68cd920..767b6d78a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java @@ -71,7 +71,8 @@ public final class PriorityDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return upstream.getUri(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 1f0313594b..ff032a4ed0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; @@ -22,6 +24,7 @@ import android.net.Uri; import androidx.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.FileInputStream; import java.io.IOException; @@ -64,9 +67,9 @@ public final class RawResourceDataSource extends BaseDataSource { private final Resources resources; - private @Nullable Uri uri; - private @Nullable AssetFileDescriptor assetFileDescriptor; - private @Nullable InputStream inputStream; + @Nullable private Uri uri; + @Nullable private AssetFileDescriptor assetFileDescriptor; + @Nullable private InputStream inputStream; private long bytesRemaining; private boolean opened; @@ -78,38 +81,31 @@ public final class RawResourceDataSource extends BaseDataSource { this.resources = context.getResources(); } - /** - * @param context A context. - * @param listener An optional listener. - * @deprecated Use {@link #RawResourceDataSource(Context)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public RawResourceDataSource(Context context, @Nullable TransferListener listener) { - this(context); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws RawResourceDataSourceException { try { - uri = dataSpec.uri; + Uri uri = dataSpec.uri; + this.uri = uri; if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) { throw new RawResourceDataSourceException("URI must use scheme " + RAW_RESOURCE_SCHEME); } int resourceId; try { - resourceId = Integer.parseInt(uri.getLastPathSegment()); + resourceId = Integer.parseInt(Assertions.checkNotNull(uri.getLastPathSegment())); } catch (NumberFormatException e) { throw new RawResourceDataSourceException("Resource identifier must be an integer."); } transferInitializing(dataSpec); - assetFileDescriptor = resources.openRawResourceFd(resourceId); - inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId); + this.assetFileDescriptor = assetFileDescriptor; + if (assetFileDescriptor == null) { + throw new RawResourceDataSourceException("Resource is compressed: " + uri); + } + FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + this.inputStream = inputStream; + inputStream.skip(assetFileDescriptor.getStartOffset()); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { @@ -147,7 +143,7 @@ public final class RawResourceDataSource extends BaseDataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new RawResourceDataSourceException(e); } @@ -167,7 +163,8 @@ public final class RawResourceDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java new file mode 100644 index 0000000000..99f0dee207 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ResolvingDataSource.java @@ -0,0 +1,134 @@ +/* + * 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.upstream; + +import android.net.Uri; +import androidx.annotation.Nullable; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** {@link DataSource} wrapper allowing just-in-time resolution of {@link DataSpec DataSpecs}. */ +public final class ResolvingDataSource implements DataSource { + + /** Resolves {@link DataSpec DataSpecs}. */ + public interface Resolver { + + /** + * Resolves a {@link DataSpec} before forwarding it to the wrapped {@link DataSource}. This + * method is allowed to block until the {@link DataSpec} has been resolved. + * + *

Note that this method is called for every new connection, so caching of results is + * recommended, especially if network operations are involved. + * + * @param dataSpec The original {@link DataSpec}. + * @return The resolved {@link DataSpec}. + * @throws IOException If an {@link IOException} occurred while resolving the {@link DataSpec}. + */ + DataSpec resolveDataSpec(DataSpec dataSpec) throws IOException; + + /** + * Resolves a URI reported by {@link DataSource#getUri()} for event reporting and caching + * purposes. + * + *

Implementations do not need to overwrite this method unless they want to change the + * reported URI. + * + *

This method is not allowed to block. + * + * @param uri The URI as reported by {@link DataSource#getUri()}. + * @return The resolved URI used for event reporting and caching. + */ + default Uri resolveReportedUri(Uri uri) { + return uri; + } + } + + /** {@link DataSource.Factory} for {@link ResolvingDataSource} instances. */ + public static final class Factory implements DataSource.Factory { + + private final DataSource.Factory upstreamFactory; + private final Resolver resolver; + + /** + * Creates factory for {@link ResolvingDataSource} instances. + * + * @param upstreamFactory The wrapped {@link DataSource.Factory} handling the resolved {@link + * DataSpec DataSpecs}. + * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}. + */ + public Factory(DataSource.Factory upstreamFactory, Resolver resolver) { + this.upstreamFactory = upstreamFactory; + this.resolver = resolver; + } + + @Override + public DataSource createDataSource() { + return new ResolvingDataSource(upstreamFactory.createDataSource(), resolver); + } + } + + private final DataSource upstreamDataSource; + private final Resolver resolver; + + private boolean upstreamOpened; + + /** + * @param upstreamDataSource The wrapped {@link DataSource}. + * @param resolver The {@link Resolver} to resolve the {@link DataSpec DataSpecs}. + */ + public ResolvingDataSource(DataSource upstreamDataSource, Resolver resolver) { + this.upstreamDataSource = upstreamDataSource; + this.resolver = resolver; + } + + @Override + public void addTransferListener(TransferListener transferListener) { + upstreamDataSource.addTransferListener(transferListener); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + DataSpec resolvedDataSpec = resolver.resolveDataSpec(dataSpec); + upstreamOpened = true; + return upstreamDataSource.open(resolvedDataSpec); + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + return upstreamDataSource.read(buffer, offset, readLength); + } + + @Nullable + @Override + public Uri getUri() { + Uri reportedUri = upstreamDataSource.getUri(); + return reportedUri == null ? null : resolver.resolveReportedUri(reportedUri); + } + + @Override + public Map> getResponseHeaders() { + return upstreamDataSource.getResponseHeaders(); + } + + @Override + public void close() throws IOException { + if (upstreamOpened) { + upstreamOpened = false; + upstreamDataSource.close(); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java index b7a01505f8..6cdc381ba2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java @@ -96,7 +96,8 @@ public final class StatsDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSource.getUri(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java index ecf25f2eb6..f56f19a6ca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java @@ -80,7 +80,8 @@ public final class TeeDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return upstream.getUri(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java index e7aab31cc2..4d9b375334 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java @@ -52,11 +52,11 @@ public final class UdpDataSource extends BaseDataSource { private final byte[] packetBuffer; private final DatagramPacket packet; - private @Nullable Uri uri; - private @Nullable DatagramSocket socket; - private @Nullable MulticastSocket multicastSocket; - private @Nullable InetAddress address; - private @Nullable InetSocketAddress socketAddress; + @Nullable private Uri uri; + @Nullable private DatagramSocket socket; + @Nullable private MulticastSocket multicastSocket; + @Nullable private InetAddress address; + @Nullable private InetSocketAddress socketAddress; private boolean opened; private int packetRemaining; @@ -88,50 +88,6 @@ public final class UdpDataSource extends BaseDataSource { packet = new DatagramPacket(packetBuffer, 0, maxPacketSize); } - /** - * Constructs a new instance. - * - * @param listener An optional listener. - * @deprecated Use {@link #UdpDataSource()} and {@link #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public UdpDataSource(@Nullable TransferListener listener) { - this(listener, DEFAULT_MAX_PACKET_SIZE); - } - - /** - * Constructs a new instance. - * - * @param listener An optional listener. - * @param maxPacketSize The maximum datagram packet size, in bytes. - * @deprecated Use {@link #UdpDataSource(int)} and {@link #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public UdpDataSource(@Nullable TransferListener listener, int maxPacketSize) { - this(listener, maxPacketSize, DEFAULT_SOCKET_TIMEOUT_MILLIS); - } - - /** - * Constructs a new instance. - * - * @param listener An optional listener. - * @param maxPacketSize The maximum datagram packet size, in bytes. - * @param socketTimeoutMillis The socket timeout in milliseconds. A timeout of zero is interpreted - * as an infinite timeout. - * @deprecated Use {@link #UdpDataSource(int, int)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public UdpDataSource( - @Nullable TransferListener listener, int maxPacketSize, int socketTimeoutMillis) { - this(maxPacketSize, socketTimeoutMillis); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws UdpDataSourceException { uri = dataSpec.uri; @@ -188,7 +144,8 @@ public final class UdpDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 3de52b560c..80fecf19cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -49,7 +49,6 @@ public final class CacheDataSink implements DataSink { private final long fragmentSize; private final int bufferSize; - private boolean syncFileDescriptor; private DataSpec dataSpec; private long dataSpecFragmentSize; private File file; @@ -108,18 +107,6 @@ public final class CacheDataSink implements DataSink { this.cache = Assertions.checkNotNull(cache); this.fragmentSize = fragmentSize == C.LENGTH_UNSET ? Long.MAX_VALUE : fragmentSize; this.bufferSize = bufferSize; - syncFileDescriptor = true; - } - - /** - * Sets whether file descriptors are synced when closing output streams. - * - *

This method is experimental, and will be renamed or removed in a future release. - * - * @param syncFileDescriptor Whether file descriptors are synced when closing output streams. - */ - public void experimental_setSyncFileDescriptor(boolean syncFileDescriptor) { - this.syncFileDescriptor = syncFileDescriptor; } @Override @@ -207,9 +194,6 @@ public final class CacheDataSink implements DataSink { boolean success = false; try { outputStream.flush(); - if (syncFileDescriptor) { - underlyingFileOutputStream.getFD().sync(); - } success = true; } finally { Util.closeQuietly(outputStream); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java index 856e9db168..ce9735badd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -26,8 +26,6 @@ public final class CacheDataSinkFactory implements DataSink.Factory { private final long fragmentSize; private final int bufferSize; - private boolean syncFileDescriptor; - /** @see CacheDataSink#CacheDataSink(Cache, long) */ public CacheDataSinkFactory(Cache cache, long fragmentSize) { this(cache, fragmentSize, CacheDataSink.DEFAULT_BUFFER_SIZE); @@ -40,20 +38,8 @@ public final class CacheDataSinkFactory implements DataSink.Factory { this.bufferSize = bufferSize; } - /** - * See {@link CacheDataSink#experimental_setSyncFileDescriptor(boolean)}. - * - *

This method is experimental, and will be renamed or removed in a future release. - */ - public CacheDataSinkFactory experimental_setSyncFileDescriptor(boolean syncFileDescriptor) { - this.syncFileDescriptor = syncFileDescriptor; - return this; - } - @Override public DataSink createDataSink() { - CacheDataSink dataSink = new CacheDataSink(cache, fragmentSize, bufferSize); - dataSink.experimental_setSyncFileDescriptor(syncFileDescriptor); - return dataSink; + return new CacheDataSink(cache, fragmentSize, bufferSize); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 58b2d176cf..6e20db7bf7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -123,7 +123,7 @@ public final class CacheDataSource implements DataSource { private final Cache cache; private final DataSource cacheReadDataSource; - private final @Nullable DataSource cacheWriteDataSource; + @Nullable private final DataSource cacheWriteDataSource; private final DataSource upstreamDataSource; private final CacheKeyFactory cacheKeyFactory; @Nullable private final EventListener eventListener; @@ -132,16 +132,17 @@ public final class CacheDataSource implements DataSource { private final boolean ignoreCacheOnError; private final boolean ignoreCacheForUnsetLengthRequests; - private @Nullable DataSource currentDataSource; + @Nullable private DataSource currentDataSource; private boolean currentDataSpecLengthUnset; - private @Nullable Uri uri; - private @Nullable Uri actualUri; - private @HttpMethod int httpMethod; + @Nullable private Uri uri; + @Nullable private Uri actualUri; + @HttpMethod private int httpMethod; + @Nullable private byte[] httpBody; private int flags; - private @Nullable String key; + @Nullable private String key; private long readPosition; private long bytesRemaining; - private @Nullable CacheSpan currentHoleSpan; + @Nullable private CacheSpan currentHoleSpan; private boolean seenCacheError; private boolean currentRequestIgnoresCache; private long totalCachedBytesRead; @@ -261,6 +262,7 @@ public final class CacheDataSource implements DataSource { uri = dataSpec.uri; actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); httpMethod = dataSpec.httpMethod; + httpBody = dataSpec.httpBody; flags = dataSpec.flags; readPosition = dataSpec.position; @@ -319,7 +321,7 @@ public final class CacheDataSource implements DataSource { } return bytesRead; } catch (IOException e) { - if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { + if (currentDataSpecLengthUnset && CacheUtil.isCausedByPositionOutOfRange(e)) { setNoBytesRemainingAndMaybeStoreLength(); return C.RESULT_END_OF_INPUT; } @@ -329,7 +331,8 @@ public final class CacheDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return actualUri; } @@ -346,6 +349,7 @@ public final class CacheDataSource implements DataSource { uri = null; actualUri = null; httpMethod = DataSpec.HTTP_METHOD_GET; + httpBody = null; notifyBytesRead(); try { closeCurrentSource(); @@ -392,7 +396,7 @@ public final class CacheDataSource implements DataSource { nextDataSource = upstreamDataSource; nextDataSpec = new DataSpec( - uri, httpMethod, null, readPosition, readPosition, bytesRemaining, key, flags); + uri, httpMethod, httpBody, readPosition, readPosition, bytesRemaining, key, flags); } else if (nextSpan.isCached) { // Data is cached, read from cache. Uri fileUri = Uri.fromFile(nextSpan.file); @@ -415,7 +419,7 @@ public final class CacheDataSource implements DataSource { } } nextDataSpec = - new DataSpec(uri, httpMethod, null, readPosition, readPosition, length, key, flags); + new DataSpec(uri, httpMethod, httpBody, readPosition, readPosition, length, key, flags); if (cacheWriteDataSource != null) { nextDataSource = cacheWriteDataSource; } else { @@ -484,20 +488,6 @@ public final class CacheDataSource implements DataSource { return redirectedUri != null ? redirectedUri : defaultUri; } - private static boolean isCausedByPositionOutOfRange(IOException e) { - Throwable cause = e; - while (cause != null) { - if (cause instanceof DataSourceException) { - int reason = ((DataSourceException) cause).reason; - if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { - return true; - } - } - cause = cause.getCause(); - } - return false; - } - private boolean isReadingFromUpstream() { return !isReadingFromCache(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java index 2a8b393ed3..2488ae0ff3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java @@ -59,7 +59,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final DatabaseProvider databaseProvider; - @MonotonicNonNull private String tableName; + private @MonotonicNonNull String tableName; /** * Deletes index data for the specified cache. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java index 1e8cf1517d..609e933c9d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java @@ -41,10 +41,8 @@ public class CacheSpan implements Comparable { * Whether the {@link CacheSpan} is cached. */ public final boolean isCached; - /** - * The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. - */ - public final @Nullable File file; + /** The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. */ + @Nullable public final File file; /** The last touch timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. */ public final long lastTouchTimestamp; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index 219d736835..47470c5de7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -20,6 +20,7 @@ import androidx.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; @@ -78,13 +79,7 @@ public final class CacheUtil { DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) { String key = buildCacheKey(dataSpec, cacheKeyFactory); long position = dataSpec.absoluteStreamPosition; - long requestLength; - if (dataSpec.length != C.LENGTH_UNSET) { - requestLength = dataSpec.length; - } else { - long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); - requestLength = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; - } + long requestLength = getRequestLength(dataSpec, cache, key); long bytesAlreadyCached = 0; long bytesLeft = requestLength; while (bytesLeft != 0) { @@ -179,53 +174,66 @@ public final class CacheUtil { Assertions.checkNotNull(dataSource); Assertions.checkNotNull(buffer); + String key = buildCacheKey(dataSpec, cacheKeyFactory); + long bytesLeft; ProgressNotifier progressNotifier = null; if (progressListener != null) { progressNotifier = new ProgressNotifier(progressListener); Pair lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory); progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second); + bytesLeft = lengthAndBytesAlreadyCached.first; + } else { + bytesLeft = getRequestLength(dataSpec, cache, key); } - String key = buildCacheKey(dataSpec, cacheKeyFactory); long position = dataSpec.absoluteStreamPosition; - long bytesLeft; - if (dataSpec.length != C.LENGTH_UNSET) { - bytesLeft = dataSpec.length; - } else { - long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); - bytesLeft = contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - position; - } + boolean lengthUnset = bytesLeft == C.LENGTH_UNSET; while (bytesLeft != 0) { throwExceptionIfInterruptedOrCancelled(isCanceled); long blockLength = - cache.getCachedLength( - key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE); + cache.getCachedLength(key, position, lengthUnset ? Long.MAX_VALUE : bytesLeft); if (blockLength > 0) { // Skip already cached data. } else { // There is a hole in the cache which is at least "-blockLength" long. blockLength = -blockLength; + long length = blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength; + boolean isLastBlock = length == bytesLeft; long read = readAndDiscard( dataSpec, position, - blockLength, + length, dataSource, buffer, priorityTaskManager, priority, progressNotifier, + isLastBlock, isCanceled); if (read < blockLength) { // Reached to the end of the data. - if (enableEOFException && bytesLeft != C.LENGTH_UNSET) { + if (enableEOFException && !lengthUnset) { throw new EOFException(); } break; } } position += blockLength; - bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength; + if (!lengthUnset) { + bytesLeft -= blockLength; + } + } + } + + private static long getRequestLength(DataSpec dataSpec, Cache cache, String key) { + if (dataSpec.length != C.LENGTH_UNSET) { + return dataSpec.length; + } else { + long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key)); + return contentLength == C.LENGTH_UNSET + ? C.LENGTH_UNSET + : contentLength - dataSpec.absoluteStreamPosition; } } @@ -242,6 +250,7 @@ public final class CacheUtil { * caching. * @param priority The priority of this task. * @param progressNotifier A notifier through which to report progress updates, or {@code null}. + * @param isLastBlock Whether this read block is the last block of the content. * @param isCanceled An optional flag that will interrupt caching if set to true. * @return Number of read bytes, or 0 if no data is available because the end of the opened range * has been reached. @@ -255,54 +264,64 @@ public final class CacheUtil { PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressNotifier progressNotifier, + boolean isLastBlock, AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; + long initialPositionOffset = positionOffset; + long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET; while (true) { if (priorityTaskManager != null) { // Wait for any other thread with higher priority to finish its job. priorityTaskManager.proceed(priority); } + throwExceptionIfInterruptedOrCancelled(isCanceled); try { - throwExceptionIfInterruptedOrCancelled(isCanceled); - // Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in - // case the given length exceeds the end of input. - dataSpec = - new DataSpec( - dataSpec.uri, - dataSpec.httpMethod, - dataSpec.httpBody, - absoluteStreamPosition, - /* position= */ dataSpec.position + positionOffset, - C.LENGTH_UNSET, - dataSpec.key, - dataSpec.flags); - long resolvedLength = dataSource.open(dataSpec); - if (progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { + long resolvedLength = C.LENGTH_UNSET; + boolean isDataSourceOpen = false; + if (endOffset != C.POSITION_UNSET) { + // If a specific length is given, first try to open the data source for that length to + // avoid more data then required to be requested. If the given length exceeds the end of + // input we will get a "position out of range" error. In that case try to open the source + // again with unset length. + try { + resolvedLength = + dataSource.open(dataSpec.subrange(positionOffset, endOffset - positionOffset)); + isDataSourceOpen = true; + } catch (IOException exception) { + if (!isLastBlock || !isCausedByPositionOutOfRange(exception)) { + throw exception; + } + Util.closeQuietly(dataSource); + } + } + if (!isDataSourceOpen) { + resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET)); + } + if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) { progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength); } - long totalBytesRead = 0; - while (totalBytesRead != length) { + while (positionOffset != endOffset) { throwExceptionIfInterruptedOrCancelled(isCanceled); int bytesRead = dataSource.read( buffer, 0, - length != C.LENGTH_UNSET - ? (int) Math.min(buffer.length, length - totalBytesRead) + endOffset != C.POSITION_UNSET + ? (int) Math.min(buffer.length, endOffset - positionOffset) : buffer.length); if (bytesRead == C.RESULT_END_OF_INPUT) { if (progressNotifier != null) { - progressNotifier.onRequestLengthResolved(positionOffset + totalBytesRead); + progressNotifier.onRequestLengthResolved(positionOffset); } break; } - totalBytesRead += bytesRead; + positionOffset += bytesRead; if (progressNotifier != null) { progressNotifier.onBytesCached(bytesRead); } } - return totalBytesRead; + return positionOffset - initialPositionOffset; } catch (PriorityTaskManager.PriorityTooLowException exception) { // catch and try again } finally { @@ -340,6 +359,20 @@ public final class CacheUtil { } } + /*package*/ static boolean isCausedByPositionOutOfRange(IOException e) { + Throwable cause = e; + while (cause != null) { + if (cause instanceof DataSourceException) { + int reason = ((DataSourceException) cause).reason; + if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { + return true; + } + } + cause = cause.getCause(); + } + return false; + } + private static String buildCacheKey( DataSpec dataSpec, @Nullable CacheKeyFactory cacheKeyFactory) { return (cacheKeyFactory != null ? cacheKeyFactory : DEFAULT_CACHE_KEY_FACTORY) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index c53c4337b5..81212b731f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -75,7 +75,7 @@ public final class SimpleCache implements Cache { private long uid; private long totalSpace; private boolean released; - @MonotonicNonNull private CacheException initializationException; + private @MonotonicNonNull CacheException initializationException; /** * Returns whether {@code cacheFolder} is locked by a {@link SimpleCache} instance. To unlock the @@ -380,20 +380,21 @@ public final class SimpleCache implements Cache { } @Override - public synchronized SimpleCacheSpan startReadWrite(String key, long position) + public synchronized CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException { Assertions.checkState(!released); checkInitialization(); while (true) { - SimpleCacheSpan span = startReadWriteNonBlocking(key, position); + CacheSpan span = startReadWriteNonBlocking(key, position); if (span != null) { return span; } else { - // Write case, lock not available. We'll be woken up when a locked span is released (if the - // released lock is for the requested key then we'll be able to make progress) or when a - // span is added to the cache (if the span is for the requested key and covers the requested - // position, then we'll become a read and be able to make progress). + // Lock not available. We'll be woken up when a span is added, or when a locked span is + // released. We'll be able to make progress when either: + // 1. A span is added for the requested key that covers the requested position, in which + // case a read can be started. + // 2. The lock for the requested key is released, in which case a write can be started. wait(); } } @@ -401,47 +402,26 @@ public final class SimpleCache implements Cache { @Override @Nullable - public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) + public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException { Assertions.checkState(!released); checkInitialization(); SimpleCacheSpan span = getSpan(key, position); - // Read case. if (span.isCached) { - if (!touchCacheSpans) { - return span; - } - String fileName = Assertions.checkNotNull(span.file).getName(); - long length = span.length; - long lastTouchTimestamp = System.currentTimeMillis(); - boolean updateFile = false; - if (fileIndex != null) { - try { - fileIndex.set(fileName, length, lastTouchTimestamp); - } catch (IOException e) { - Log.w(TAG, "Failed to update index with new touch timestamp."); - } - } else { - // Updating the file itself to incorporate the new last touch timestamp is much slower than - // updating the file index. Hence we only update the file if we don't have a file index. - updateFile = true; - } - SimpleCacheSpan newSpan = - contentIndex.get(key).setLastTouchTimestamp(span, lastTouchTimestamp, updateFile); - notifySpanTouched(span, newSpan); - return newSpan; + // Read case. + return touchSpan(key, span); } CachedContent cachedContent = contentIndex.getOrAdd(key); if (!cachedContent.isLocked()) { - // Write case, lock available. + // Write case. cachedContent.setLocked(true); return span; } - // Write case, lock not available. + // Lock not available. return null; } @@ -558,36 +538,6 @@ public final class SimpleCache implements Cache { return contentIndex.getContentMetadata(key); } - /** - * Returns the cache {@link SimpleCacheSpan} corresponding to the provided lookup {@link - * SimpleCacheSpan}. - * - *

If the lookup position is contained by an existing entry in the cache, then the returned - * {@link SimpleCacheSpan} defines the file in which the data is stored. If the lookup position is - * not contained by an existing entry, then the returned {@link SimpleCacheSpan} defines the - * maximum extents of the hole in the cache. - * - * @param key The key of the span being requested. - * @param position The position of the span being requested. - * @return The corresponding cache {@link SimpleCacheSpan}. - */ - private SimpleCacheSpan getSpan(String key, long position) { - CachedContent cachedContent = contentIndex.get(key); - if (cachedContent == null) { - return SimpleCacheSpan.createOpenHole(key, position); - } - while (true) { - SimpleCacheSpan span = cachedContent.getSpan(position); - if (span.isCached && !span.file.exists()) { - // The file has been deleted from under us. It's likely that other files will have been - // deleted too, so scan the whole in-memory representation. - removeStaleSpans(); - continue; - } - return span; - } - } - /** Ensures that the cache's in-memory representation has been initialized. */ private void initialize() { if (!cacheDir.exists()) { @@ -696,6 +646,67 @@ public final class SimpleCache implements Cache { } } + /** + * Touches a cache span, returning the updated result. If the evictor does not require cache spans + * to be touched, then this method does nothing and the span is returned without modification. + * + * @param key The key of the span being touched. + * @param span The span being touched. + * @return The updated span. + */ + private SimpleCacheSpan touchSpan(String key, SimpleCacheSpan span) { + if (!touchCacheSpans) { + return span; + } + String fileName = Assertions.checkNotNull(span.file).getName(); + long length = span.length; + long lastTouchTimestamp = System.currentTimeMillis(); + boolean updateFile = false; + if (fileIndex != null) { + try { + fileIndex.set(fileName, length, lastTouchTimestamp); + } catch (IOException e) { + Log.w(TAG, "Failed to update index with new touch timestamp."); + } + } else { + // Updating the file itself to incorporate the new last touch timestamp is much slower than + // updating the file index. Hence we only update the file if we don't have a file index. + updateFile = true; + } + SimpleCacheSpan newSpan = + contentIndex.get(key).setLastTouchTimestamp(span, lastTouchTimestamp, updateFile); + notifySpanTouched(span, newSpan); + return newSpan; + } + + /** + * Returns the cache span corresponding to the provided lookup span. + * + *

If the lookup position is contained by an existing entry in the cache, then the returned + * span defines the file in which the data is stored. If the lookup position is not contained by + * an existing entry, then the returned span defines the maximum extents of the hole in the cache. + * + * @param key The key of the span being requested. + * @param position The position of the span being requested. + * @return The corresponding cache {@link SimpleCacheSpan}. + */ + private SimpleCacheSpan getSpan(String key, long position) { + CachedContent cachedContent = contentIndex.get(key); + if (cachedContent == null) { + return SimpleCacheSpan.createOpenHole(key, position); + } + while (true) { + SimpleCacheSpan span = cachedContent.getSpan(position); + if (span.isCached && !span.file.exists()) { + // The file has been deleted from under us. It's likely that other files will have been + // deleted too, so scan the whole in-memory representation. + removeStaleSpans(); + continue; + } + return span; + } + } + /** * Adds a cached span to the in-memory representation. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java index ccf9a5b3f5..522fdc9a3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.upstream.crypto; +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.Nullable; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; @@ -27,9 +30,9 @@ public final class AesCipherDataSink implements DataSink { private final DataSink wrappedDataSink; private final byte[] secretKey; - private final byte[] scratch; + @Nullable private final byte[] scratch; - private AesFlushingCipher cipher; + @Nullable private AesFlushingCipher cipher; /** * Create an instance whose {@code write} methods have the side effect of overwriting the input @@ -52,9 +55,10 @@ public final class AesCipherDataSink implements DataSink { * @param scratch Scratch space. Data is decrypted into this array before being written to the * wrapped {@link DataSink}. It should be of appropriate size for the expected writes. If a * write is larger than the size of this array the write will still succeed, but multiple - * cipher calls will be required to complete the operation. + * cipher calls will be required to complete the operation. If {@code null} then decryption + * will overwrite the input {@code data}. */ - public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, byte[] scratch) { + public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, @Nullable byte[] scratch) { this.wrappedDataSink = wrappedDataSink; this.secretKey = secretKey; this.scratch = scratch; @@ -72,15 +76,16 @@ public final class AesCipherDataSink implements DataSink { public void write(byte[] data, int offset, int length) throws IOException { if (scratch == null) { // In-place mode. Writes over the input data. - cipher.updateInPlace(data, offset, length); + castNonNull(cipher).updateInPlace(data, offset, length); wrappedDataSink.write(data, offset, length); } else { // Use scratch space. The original data remains intact. int bytesProcessed = 0; while (bytesProcessed < length) { int bytesToProcess = Math.min(length - bytesProcessed, scratch.length); - cipher.update(data, offset + bytesProcessed, bytesToProcess, scratch, 0); - wrappedDataSink.write(scratch, 0, bytesToProcess); + castNonNull(cipher) + .update(data, offset + bytesProcessed, bytesToProcess, scratch, /* outOffset= */ 0); + wrappedDataSink.write(scratch, /* offset= */ 0, bytesToProcess); bytesProcessed += bytesToProcess; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java index 7a7af6b8a4..0910c63c19 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream.crypto; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -34,7 +36,7 @@ public final class AesCipherDataSource implements DataSource { private final DataSource upstream; private final byte[] secretKey; - private @Nullable AesFlushingCipher cipher; + @Nullable private AesFlushingCipher cipher; public AesCipherDataSource(byte[] secretKey, DataSource upstream) { this.upstream = upstream; @@ -64,12 +66,13 @@ public final class AesCipherDataSource implements DataSource { if (read == C.RESULT_END_OF_INPUT) { return C.RESULT_END_OF_INPUT; } - cipher.updateInPlace(data, offset, read); + castNonNull(cipher).updateInPlace(data, offset, read); return read; } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return upstream.getUri(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java index ff8841fa9c..3418f46ed0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream.crypto; +import androidx.annotation.Nullable; + /** * Utility functions for the crypto package. */ @@ -24,10 +26,10 @@ package com.google.android.exoplayer2.upstream.crypto; /** * Returns the hash value of the input as a long using the 64 bit FNV-1a hash function. The hash - * values produced by this function are less likely to collide than those produced by - * {@link #hashCode()}. + * values produced by this function are less likely to collide than those produced by {@link + * #hashCode()}. */ - public static long getFNV64Hash(String input) { + public static long getFNV64Hash(@Nullable String input) { if (input == null) { return 0; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java index 33b50934f1..e72e72c3c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java @@ -83,12 +83,12 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL private final Handler handler; private final int[] textureIdHolder; - private final @Nullable TextureImageListener callback; + @Nullable private final TextureImageListener callback; - private @Nullable EGLDisplay display; - private @Nullable EGLContext context; - private @Nullable EGLSurface surface; - private @Nullable SurfaceTexture texture; + @Nullable private EGLDisplay display; + @Nullable private EGLContext context; + @Nullable private EGLSurface surface; + @Nullable private SurfaceTexture texture; /** * @param handler The {@link Handler} that will be used to call {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 7a2ea5daf2..cde9a351d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -54,7 +54,7 @@ public class EventLogger implements AnalyticsListener { TIME_FORMAT.setGroupingUsed(false); } - private final @Nullable MappingTrackSelector trackSelector; + @Nullable private final MappingTrackSelector trackSelector; private final String tag; private final Timeline.Window window; private final Timeline.Period period; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java similarity index 64% rename from library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 0df39e103d..2c814294af 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -15,12 +15,18 @@ */ package com.google.android.exoplayer2.util; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import java.util.ArrayList; +import java.util.List; -/** - * Holder for FLAC stream info. - */ -public final class FlacStreamInfo { +/** Holder for FLAC metadata. */ +public final class FlacStreamMetadata { + + private static final String TAG = "FlacStreamMetadata"; public final int minBlockSize; public final int maxBlockSize; @@ -30,16 +36,19 @@ public final class FlacStreamInfo { public final int channels; public final int bitsPerSample; public final long totalSamples; + @Nullable public final Metadata metadata; + + private static final String SEPARATOR = "="; /** - * Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure. + * Parses binary FLAC stream info metadata. * - * @param data An array holding FLAC stream info metadata structure - * @param offset Offset of the structure in the array + * @param data An array containing binary FLAC stream info metadata. + * @param offset The offset of the stream info metadata in {@code data}. * @see FLAC format * METADATA_BLOCK_STREAMINFO */ - public FlacStreamInfo(byte[] data, int offset) { + public FlacStreamMetadata(byte[] data, int offset) { ParsableBitArray scratch = new ParsableBitArray(data); scratch.setPosition(offset * 8); this.minBlockSize = scratch.readBits(16); @@ -49,14 +58,11 @@ public final class FlacStreamInfo { this.sampleRate = scratch.readBits(20); this.channels = scratch.readBits(3) + 1; this.bitsPerSample = scratch.readBits(5) + 1; - this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) - | (scratch.readBits(32) & 0xFFFFFFFFL); - // Remaining 16 bytes is md5 value + this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL); + this.metadata = null; } /** - * Constructs a FlacStreamInfo given the parameters. - * * @param minBlockSize Minimum block size of the FLAC stream. * @param maxBlockSize Maximum block size of the FLAC stream. * @param minFrameSize Minimum frame size of the FLAC stream. @@ -65,10 +71,16 @@ public final class FlacStreamInfo { * @param channels Number of channels of the FLAC stream. * @param bitsPerSample Number of bits per sample of the FLAC stream. * @param totalSamples Total samples of the FLAC stream. + * @param vorbisComments Vorbis comments. Each entry must be in key=value form. + * @param pictureFrames Picture frames. * @see FLAC format * METADATA_BLOCK_STREAMINFO + * @see FLAC format + * METADATA_BLOCK_VORBIS_COMMENT + * @see FLAC format + * METADATA_BLOCK_PICTURE */ - public FlacStreamInfo( + public FlacStreamMetadata( int minBlockSize, int maxBlockSize, int minFrameSize, @@ -76,7 +88,9 @@ public final class FlacStreamInfo { int sampleRate, int channels, int bitsPerSample, - long totalSamples) { + long totalSamples, + List vorbisComments, + List pictureFrames) { this.minBlockSize = minBlockSize; this.maxBlockSize = maxBlockSize; this.minFrameSize = minFrameSize; @@ -85,6 +99,7 @@ public final class FlacStreamInfo { this.channels = channels; this.bitsPerSample = bitsPerSample; this.totalSamples = totalSamples; + this.metadata = buildMetadata(vorbisComments, pictureFrames); } /** Returns the maximum size for a decoded frame from the FLAC stream. */ @@ -126,4 +141,27 @@ public final class FlacStreamInfo { } return approxBytesPerFrame; } + + @Nullable + private static Metadata buildMetadata( + List vorbisComments, List pictureFrames) { + if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) { + return null; + } + + ArrayList metadataEntries = new ArrayList<>(); + for (int i = 0; i < vorbisComments.size(); i++) { + String vorbisComment = vorbisComments.get(i); + String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR); + if (keyAndValue.length != 2) { + Log.w(TAG, "Failed to parse vorbis comment: " + vorbisComment); + } else { + VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]); + metadataEntries.add(entry); + } + } + metadataEntries.addAll(pictureFrames); + + return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java index a10298e456..e9f08a35c9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaClock.java @@ -28,13 +28,12 @@ public interface MediaClock { long getPositionUs(); /** - * Attempts to set the playback parameters and returns the active playback parameters, which may - * differ from those passed in. + * Attempts to set the playback parameters. The media clock may override these parameters if they + * are not supported. * - * @param playbackParameters The playback parameters. - * @return The active playback parameters. + * @param playbackParameters The playback parameters to attempt to set. */ - PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters); + void setPlaybackParameters(PlaybackParameters playbackParameters); /** * Returns the active playback parameters. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index e603f76dbc..61457c308d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -348,8 +348,9 @@ public final class MimeTypes { case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: - case MimeTypes.AUDIO_E_AC3_JOC: return C.ENCODING_E_AC3; + case MimeTypes.AUDIO_E_AC3_JOC: + return C.ENCODING_E_AC3_JOC; case MimeTypes.AUDIO_AC4: return C.ENCODING_AC4; case MimeTypes.AUDIO_DTS: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java new file mode 100644 index 0000000000..bd7a70eba0 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java @@ -0,0 +1,36 @@ +/* + * 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.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierDefault; +// import kotlin.annotations.jvm.MigrationStatus; +// import kotlin.annotations.jvm.UnderMigration; + +/** + * Annotation to declare all type usages in the annotated instance as {@link Nonnull}, unless + * explicitly marked with a nullable annotation. + */ +@Nonnull +@TypeQualifierDefault(ElementType.TYPE_USE) +// TODO(internal: b/138703808): Uncomment to ensure Kotlin issues compiler errors when non-null +// types are used incorrectly. +// @UnderMigration(status = MigrationStatus.STRICT) +@Retention(RetentionPolicy.CLASS) +public @interface NonNullApi {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java index 4cd03f566d..756494f9d0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java @@ -61,6 +61,14 @@ public final class NotificationUtil { /** @see NotificationManager#IMPORTANCE_HIGH */ public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH; + /** @deprecated Use {@link #createNotificationChannel(Context, String, int, int, int)}. */ + @Deprecated + public static void createNotificationChannel( + Context context, String id, @StringRes int nameResourceId, @Importance int importance) { + createNotificationChannel( + context, id, nameResourceId, /* descriptionResourceId= */ 0, importance); + } + /** * Creates a notification channel that notifications can be posted to. See {@link * NotificationChannel} and {@link @@ -70,21 +78,33 @@ public final class NotificationUtil { * @param id The id of the channel. Must be unique per package. The value may be truncated if it's * too long. * @param nameResourceId A string resource identifier for the user visible name of the channel. - * You can rename this channel when the system locale changes by listening for the {@link - * Intent#ACTION_LOCALE_CHANGED} broadcast. The recommended maximum length is 40 characters. - * The value may be truncated if it is too long. + * The recommended maximum length is 40 characters. The string may be truncated if it's too + * long. You can rename the channel when the system locale changes by listening for the {@link + * Intent#ACTION_LOCALE_CHANGED} broadcast. + * @param descriptionResourceId A string resource identifier for the user visible description of + * the channel, or 0 if no description is provided. The recommended maximum length is 300 + * characters. The value may be truncated if it is too long. You can change the description of + * the channel when the system locale changes by listening for the {@link + * Intent#ACTION_LOCALE_CHANGED} broadcast. * @param importance The importance of the channel. This controls how interruptive notifications * posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link * #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}. */ public static void createNotificationChannel( - Context context, String id, @StringRes int nameResourceId, @Importance int importance) { + Context context, + String id, + @StringRes int nameResourceId, + @StringRes int descriptionResourceId, + @Importance int importance) { if (Util.SDK_INT >= 26) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationChannel channel = new NotificationChannel(id, context.getString(nameResourceId), importance); + if (descriptionResourceId != 0) { + channel.setDescription(context.getString(descriptionResourceId)); + } notificationManager.createNotificationChannel(channel); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 0c5116624e..67686ad64f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -490,7 +490,8 @@ public final class ParsableByteArray { * @return The string not including any terminating NUL byte, or null if the end of the data has * already been reached. */ - public @Nullable String readNullTerminatedString() { + @Nullable + public String readNullTerminatedString() { if (bytesLeft() == 0) { return null; } @@ -516,7 +517,8 @@ public final class ParsableByteArray { * @return The line not including any line-termination characters, or null if the end of the data * has already been reached. */ - public @Nullable String readLine() { + @Nullable + public String readLine() { if (bytesLeft() == 0) { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java index b1f53416fb..e5f9aa645f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java @@ -88,13 +88,12 @@ public final class StandaloneMediaClock implements MediaClock { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { // Store the current position as the new base, in case the playback speed has changed. if (started) { resetPosition(getPositionUs()); } this.playbackParameters = playbackParameters; - return playbackParameters; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java index 3ac76eb54c..da5d9bafeb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java @@ -97,7 +97,8 @@ public final class TimedValueQueue { * @return The value with the closest timestamp or null if the buffer is empty or there is no * older value and {@code onlyOlder} is true. */ - private @Nullable V poll(long timestamp, boolean onlyOlder) { + @Nullable + private V poll(long timestamp, boolean onlyOlder) { V value = null; long previousTimeDiff = Long.MAX_VALUE; while (size > 0) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 97bcb68708..e700fc6751 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -71,6 +71,7 @@ import java.util.Calendar; import java.util.Collections; import java.util.Formatter; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; @@ -135,6 +136,10 @@ public final class Util { + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); + // Android standardizes to ISO 639-1 2-letter codes and provides no way to map a 3-letter + // ISO 639-2 code back to the corresponding 2-letter code. + @Nullable private static HashMap languageTagIso3ToIso2; + private Util() {} /** @@ -315,6 +320,21 @@ public final class Util { return Arrays.copyOf(input, length); } + /** + * Copies a subset of an array. + * + * @param input The input array. + * @param from The start the range to be copied, inclusive + * @param to The end of the range to be copied, exclusive. + * @return The copied array. + */ + @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"}) + public static T[] nullSafeArrayCopyOfRange(T[] input, int from, int to) { + Assertions.checkArgument(0 <= from); + Assertions.checkArgument(to <= input.length); + return Arrays.copyOfRange(input, from, to); + } + /** * Concatenates two non-null type arrays. * @@ -450,18 +470,31 @@ public final class Util { if (language == null) { return null; } - try { - Locale locale = getLocaleForLanguageTag(language); - int localeLanguageLength = locale.getLanguage().length(); - String normLanguage = locale.getISO3Language(); - if (normLanguage.isEmpty()) { - return toLowerInvariant(language); - } - String normTag = getLocaleLanguageTag(locale); - return toLowerInvariant(normLanguage + normTag.substring(localeLanguageLength)); - } catch (MissingResourceException e) { - return toLowerInvariant(language); + // Locale data (especially for API < 21) may produce tags with '_' instead of the + // standard-conformant '-'. + String normalizedTag = language.replace('_', '-'); + if (Util.SDK_INT >= 21) { + // Filters out ill-formed sub-tags, replaces deprecated tags and normalizes all valid tags. + normalizedTag = normalizeLanguageCodeSyntaxV21(normalizedTag); } + if (normalizedTag.isEmpty() || "und".equals(normalizedTag)) { + // Tag isn't valid, keep using the original. + normalizedTag = language; + } + normalizedTag = Util.toLowerInvariant(normalizedTag); + String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0]; + if (mainLanguage.length() == 3) { + // 3-letter ISO 639-2/B or ISO 639-2/T language codes will not be converted to 2-letter ISO + // 639-1 codes automatically. + if (languageTagIso3ToIso2 == null) { + languageTagIso3ToIso2 = createIso3ToIso2Map(); + } + String iso2Language = languageTagIso3ToIso2.get(mainLanguage); + if (iso2Language != null) { + normalizedTag = iso2Language + normalizedTag.substring(/* beginIndex= */ 3); + } + } + return normalizedTag; } /** @@ -1713,7 +1746,12 @@ public final class Util { if (connectivityManager == null) { return C.NETWORK_TYPE_UNKNOWN; } - networkInfo = connectivityManager.getActiveNetworkInfo(); + try { + networkInfo = connectivityManager.getActiveNetworkInfo(); + } catch (SecurityException e) { + // Expected if permission was revoked. + return C.NETWORK_TYPE_UNKNOWN; + } if (networkInfo == null || !networkInfo.isConnected()) { return C.NETWORK_TYPE_OFFLINE; } @@ -1950,32 +1988,25 @@ public final class Util { } private static String[] getSystemLocales() { + Configuration config = Resources.getSystem().getConfiguration(); return SDK_INT >= 24 - ? getSystemLocalesV24() - : new String[] {getLocaleLanguageTag(Resources.getSystem().getConfiguration().locale)}; + ? getSystemLocalesV24(config) + : SDK_INT >= 21 ? getSystemLocaleV21(config) : new String[] {config.locale.toString()}; } @TargetApi(24) - private static String[] getSystemLocalesV24() { - return Util.split(Resources.getSystem().getConfiguration().getLocales().toLanguageTags(), ","); - } - - private static Locale getLocaleForLanguageTag(String languageTag) { - return Util.SDK_INT >= 21 ? getLocaleForLanguageTagV21(languageTag) : new Locale(languageTag); + private static String[] getSystemLocalesV24(Configuration config) { + return Util.split(config.getLocales().toLanguageTags(), ","); } @TargetApi(21) - private static Locale getLocaleForLanguageTagV21(String languageTag) { - return Locale.forLanguageTag(languageTag); - } - - private static String getLocaleLanguageTag(Locale locale) { - return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString(); + private static String[] getSystemLocaleV21(Configuration config) { + return new String[] {config.locale.toLanguageTag()}; } @TargetApi(21) - private static String getLocaleLanguageTagV21(Locale locale) { - return locale.toLanguageTag(); + private static String normalizeLanguageCodeSyntaxV21(String languageTag) { + return Locale.forLanguageTag(languageTag).toLanguageTag(); } private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) { @@ -2008,6 +2039,54 @@ public final class Util { } } + private static HashMap createIso3ToIso2Map() { + String[] iso2Languages = Locale.getISOLanguages(); + HashMap iso3ToIso2 = + new HashMap<>( + /* initialCapacity= */ iso2Languages.length + iso3BibliographicalToIso2.length); + for (String iso2 : iso2Languages) { + try { + // This returns the ISO 639-2/T code for the language. + String iso3 = new Locale(iso2).getISO3Language(); + if (!TextUtils.isEmpty(iso3)) { + iso3ToIso2.put(iso3, iso2); + } + } catch (MissingResourceException e) { + // Shouldn't happen for list of known languages, but we don't want to throw either. + } + } + // Add additional ISO 639-2/B codes to mapping. + for (int i = 0; i < iso3BibliographicalToIso2.length; i += 2) { + iso3ToIso2.put(iso3BibliographicalToIso2[i], iso3BibliographicalToIso2[i + 1]); + } + return iso3ToIso2; + } + + // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. + private static final String[] iso3BibliographicalToIso2 = + new String[] { + "alb", "sq", + "arm", "hy", + "baq", "eu", + "bur", "my", + "tib", "bo", + "chi", "zh", + "cze", "cs", + "dut", "nl", + "ger", "de", + "gre", "el", + "fre", "fr", + "geo", "ka", + "ice", "is", + "mac", "mk", + "mao", "mi", + "may", "ms", + "per", "fa", + "rum", "ro", + "slo", "sk", + "wel", "cy" + }; + /** * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order * "most significant bit first". diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java index 1b3943caf7..ed2ca9c034 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java @@ -51,7 +51,7 @@ public final class ColorInfo implements Parcelable { public final int colorTransfer; /** HdrStaticInfo as defined in CTA-861.3, or null if none specified. */ - public final @Nullable byte[] hdrStaticInfo; + @Nullable public final byte[] hdrStaticInfo; // Lazily initialized hashcode. private int hashCode; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index f302279f06..920d569fd3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -158,9 +158,9 @@ public final class DummySurface extends Surface { private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexture; private @MonotonicNonNull Handler handler; - private @Nullable Error initError; - private @Nullable RuntimeException initException; - private @Nullable DummySurface surface; + @Nullable private Error initError; + @Nullable private RuntimeException initException; + @Nullable private DummySurface surface; public DummySurfaceThread() { super("dummySurface"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java b/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java index 727883f678..bb11ef0005 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java @@ -27,7 +27,7 @@ import java.util.List; */ public final class HevcConfig { - public final @Nullable List initializationData; + @Nullable public final List initializationData; public final int nalUnitLengthFieldLength; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index fe9996bfc2..2ab7e61378 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -92,6 +92,23 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { */ private static final float INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR = 1.5f; + /** A {@link DecoderException} with additional surface information. */ + public static final class VideoDecoderException extends DecoderException { + + /** The {@link System#identityHashCode(Object)} of the surface when the exception occurred. */ + public final int surfaceIdentityHashCode; + + /** Whether the surface was valid when the exception occurred. */ + public final boolean isSurfaceValid; + + public VideoDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo, @Nullable Surface surface) { + super(cause, codecInfo); + surfaceIdentityHashCode = System.identityHashCode(surface); + isSurfaceValid = surface == null || surface.isValid(); + } + } + private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround; private static boolean deviceNeedsSetOutputSurfaceWorkaround; @@ -138,7 +155,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private long lastInputTimeUs; private long outputStreamOffsetUs; private int pendingOutputStreamOffsetCount; - private @Nullable VideoFrameMetadataListener frameMetadataListener; + @Nullable private VideoFrameMetadataListener frameMetadataListener; /** * @param context A context. @@ -319,7 +336,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (decoderInfos.isEmpty()) { return FORMAT_UNSUPPORTED_SUBTYPE; } - if (!supportsFormatDrm(drmSessionManager, drmInitData)) { + boolean supportsFormatDrm = + format.drmInitData == null + || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, format.drmInitData)); + if (!supportsFormatDrm) { return FORMAT_UNSUPPORTED_DRM; } // Check capabilities for the first decoder in the list, which takes priority. @@ -368,8 +390,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { // Fallback to primary decoders for H.265/HEVC or H.264/AVC for the relevant DV profiles. - Pair codecProfileAndLevel = - MediaCodecUtil.getCodecProfileAndLevel(format.codecs); + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel != null) { int profile = codecProfileAndLevel.first; if (profile == 4 || profile == 8) { @@ -581,12 +602,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { MediaCodec codec, Format format, MediaCrypto crypto, - float codecOperatingRate) - throws DecoderQueryException { + float codecOperatingRate) { + String codecMimeType = codecInfo.codecMimeType; codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); MediaFormat mediaFormat = getMediaFormat( format, + codecMimeType, codecMaxValues, codecOperatingRate, deviceNeedsNoPostProcessWorkaround, @@ -712,7 +734,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException { if (initialPositionUs == C.TIME_UNSET) { @@ -721,7 +744,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; - if (shouldSkip) { + if (isDecodeOnlyBuffer && !isLastBuffer) { skipOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -769,10 +792,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs); earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; - if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) + if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer) && maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) { return false; - } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { + } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) { dropOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -840,8 +863,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { /** * Returns the offset that should be subtracted from {@code bufferPresentationTimeUs} in {@link - * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, Format)} to - * get the playback position with respect to the media. + * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, boolean, + * Format)} to get the playback position with respect to the media. */ protected long getOutputStreamOffsetUs() { return outputStreamOffsetUs; @@ -893,9 +916,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * indicates that the buffer is late. * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, * measured at the start of the current iteration of the rendering loop. + * @param isLastBuffer Whether the buffer is the last buffer in the current stream. */ - protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { - return isBufferLate(earlyUs); + protected boolean shouldDropOutputBuffer( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + return isBufferLate(earlyUs) && !isLastBuffer; } /** @@ -906,9 +931,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * negative value indicates that the buffer is late. * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, * measured at the start of the current iteration of the rendering loop. + * @param isLastBuffer Whether the buffer is the last buffer in the current stream. */ - protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) { - return isBufferVeryLate(earlyUs); + protected boolean shouldDropBuffersToKeyframe( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + return isBufferVeryLate(earlyUs) && !isLastBuffer; } /** @@ -1139,6 +1166,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * Returns the framework {@link MediaFormat} that should be used to configure the decoder. * * @param format The format of media. + * @param codecMimeType The MIME type handled by the codec. * @param codecMaxValues Codec max values that should be used when configuring the decoder. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * no codec operating rate should be set. @@ -1151,13 +1179,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @SuppressLint("InlinedApi") protected MediaFormat getMediaFormat( Format format, + String codecMimeType, CodecMaxValues codecMaxValues, float codecOperatingRate, boolean deviceNeedsNoPostProcessWorkaround, int tunnelingAudioSessionId) { MediaFormat mediaFormat = new MediaFormat(); // Set format parameters that should always be set. - mediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); + mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType); mediaFormat.setInteger(MediaFormat.KEY_WIDTH, format.width); mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, format.height); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); @@ -1168,8 +1197,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { // Some phones require the profile to be set on the codec. // See https://github.com/google/ExoPlayer/pull/5438. - Pair codecProfileAndLevel = - MediaCodecUtil.getCodecProfileAndLevel(format.codecs); + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel != null) { MediaFormatUtil.maybeSetInteger( mediaFormat, MediaFormat.KEY_PROFILE, codecProfileAndLevel.first); @@ -1205,11 +1233,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param format The format for which the codec is being configured. * @param streamFormats The possible stream formats. * @return Suitable {@link CodecMaxValues}. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ protected CodecMaxValues getCodecMaxValues( - MediaCodecInfo codecInfo, Format format, Format[] streamFormats) - throws DecoderQueryException { + MediaCodecInfo codecInfo, Format format, Format[] streamFormats) { int maxWidth = format.width; int maxHeight = format.height; int maxInputSize = getMaxInputSize(codecInfo, format); @@ -1258,18 +1284,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } + @Override + protected DecoderException createDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo) { + return new VideoDecoderException(cause, codecInfo, surface); + } + /** - * Returns a maximum video size to use when configuring a codec for {@code format} in a way - * that will allow possible adaptation to other compatible formats that are expected to have the - * same aspect ratio, but whose sizes are unknown. + * Returns a maximum video size to use when configuring a codec for {@code format} in a way that + * will allow possible adaptation to other compatible formats that are expected to have the same + * aspect ratio, but whose sizes are unknown. * * @param codecInfo Information about the {@link MediaCodec} being configured. * @param format The format for which the codec is being configured. * @return The maximum video size to use, or null if the size of {@code format} should be used. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) - throws DecoderQueryException { + private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) { boolean isVerticalVideo = format.height > format.width; int formatLongEdgePx = isVerticalVideo ? format.height : format.width; int formatShortEdgePx = isVerticalVideo ? format.width : format.height; @@ -1287,12 +1317,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return alignedSize; } } else { - // Conservatively assume the codec requires 16px width and height alignment. - longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16; - shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16; - if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) { - return new Point(isVerticalVideo ? shortEdgePx : longEdgePx, - isVerticalVideo ? longEdgePx : shortEdgePx); + try { + // Conservatively assume the codec requires 16px width and height alignment. + longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16; + shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16; + if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) { + return new Point( + isVerticalVideo ? shortEdgePx : longEdgePx, + isVerticalVideo ? longEdgePx : shortEdgePx); + } + } catch (DecoderQueryException e) { + // We tried our best. Give up! + return null; } } } @@ -1455,6 +1491,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { case "1713": case "1714": case "A10-70F": + case "A10-70L": case "A1601": case "A2016a40": case "A7000-a": @@ -1597,6 +1634,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return deviceNeedsSetOutputSurfaceWorkaround; } + protected Surface getSurface() { + return surface; + } + protected static final class CodecMaxValues { public final int width; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java new file mode 100644 index 0000000000..68108af636 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java @@ -0,0 +1,40 @@ +/* + * 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.video; + +/** Thrown when a video decoder error occurs. */ +public class VideoDecoderException extends Exception { + + /** + * Creates an instance with the given message. + * + * @param message The detail message for this exception. + */ + public VideoDecoderException(String message) { + super(message); + } + + /** + * Creates an instance with the given message and cause. + * + * @param message The detail message for this exception. + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + * A null value is permitted, and indicates that the cause is nonexistent or unknown. + */ + public VideoDecoderException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java similarity index 70% rename from extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java rename to library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java index fcae9dc6bc..76742a8691 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * 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. @@ -13,19 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.ext.vp9; +package com.google.android.exoplayer2.video; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.video.ColorInfo; -/** - * Input buffer to a {@link VpxDecoder}. - */ -/* package */ final class VpxInputBuffer extends DecoderInputBuffer { +/** Input buffer to a video decoder. */ +public class VideoDecoderInputBuffer extends DecoderInputBuffer { public ColorInfo colorInfo; - public VpxInputBuffer() { + public VideoDecoderInputBuffer() { super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java new file mode 100644 index 0000000000..af0844defb --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java @@ -0,0 +1,125 @@ +/* + * 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.video; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.OutputBuffer; +import java.nio.ByteBuffer; + +/** Video decoder output buffer containing video frame data. */ +public abstract class VideoDecoderOutputBuffer 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; + + /** Decoder private data. */ + public int decoderPrivate; + + /** Output mode. */ + @C.VideoOutputMode 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; + + /** + * Initializes the buffer. + * + * @param timeUs The presentation timestamp for the buffer, in microseconds. + * @param mode The output mode. One of {@link C#VIDEO_OUTPUT_MODE_NONE}, {@link + * C#VIDEO_OUTPUT_MODE_YUV} and {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV}. + */ + public void init(long timeUs, @C.VideoOutputMode 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; + } + + // Initialize data. + if (data == null || data.capacity() < minimumYuvSize) { + data = ByteBuffer.allocateDirect(minimumYuvSize); + } else { + data.position(0); + data.limit(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; + } + + /** + * Configures the buffer for the given frame dimensions when passing actual frame data via {@link + * #decoderPrivate}. Called via JNI after decoding completes. + */ + public void initForPrivateFrame(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * Ensures that the result of multiplying individual numbers can fit into the size limit of an + * integer. + */ + private static boolean isSafeToMultiply(int a, int b) { + return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java index eb7110834b..03822be17c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java @@ -39,7 +39,7 @@ public class CameraMotionRenderer extends BaseRenderer { private final ParsableByteArray scratch; private long offsetUs; - private @Nullable CameraMotionListener listener; + @Nullable private CameraMotionListener listener; private long lastTimestampUs; public CameraMotionRenderer() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java index 527aa5db4f..eadc617ea7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java @@ -36,12 +36,12 @@ import java.util.zip.Inflater; */ public final class ProjectionDecoder { - private static final int TYPE_YTMP = Util.getIntegerCodeForString("ytmp"); - private static final int TYPE_MSHP = Util.getIntegerCodeForString("mshp"); - private static final int TYPE_RAW = Util.getIntegerCodeForString("raw "); - private static final int TYPE_DFL8 = Util.getIntegerCodeForString("dfl8"); - private static final int TYPE_MESH = Util.getIntegerCodeForString("mesh"); - private static final int TYPE_PROJ = Util.getIntegerCodeForString("proj"); + private static final int TYPE_YTMP = 0x79746d70; + private static final int TYPE_MSHP = 0x6d736870; + private static final int TYPE_RAW = 0x72617720; + private static final int TYPE_DFL8 = 0x64666c38; + private static final int TYPE_MESH = 0x6d657368; + private static final int TYPE_PROJ = 0x70726f6a; // Sanity limits to prevent a bad file from creating an OOM situation. We don't expect a mesh to // exceed these limits. diff --git a/library/core/src/test/AndroidManifest.xml b/library/core/src/test/AndroidManifest.xml index 2cf0313256..72b555a25a 100644 --- a/library/core/src/test/AndroidManifest.xml +++ b/library/core/src/test/AndroidManifest.xml @@ -14,4 +14,6 @@ limitations under the License. --> - + + + diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/webvtt/with_css_styles b/library/core/src/test/assets/webvtt/with_css_styles index 2056d50adf..c2d47d8192 100644 --- a/library/core/src/test/assets/webvtt/with_css_styles +++ b/library/core/src/test/assets/webvtt/with_css_styles @@ -13,8 +13,6 @@ STYLE ::cue(#id2) { color: peachpuff; } - -STYLE ::cue(v[voice="LaGord"]) { background-color: lime } STYLE diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java index be0f7f55c7..b6e3d7a648 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.MockitoAnnotations.initMocks; @@ -54,13 +53,14 @@ public class DefaultMediaClockTest { @Test public void standaloneResetPosition_getPositionShouldReturnSameValue() throws Exception { mediaClock.resetPosition(TEST_POSITION_US); - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); } @Test public void standaloneGetAndResetPosition_shouldNotTriggerCallback() throws Exception { mediaClock.resetPosition(TEST_POSITION_US); - mediaClock.syncAndGetPositionUs(); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verifyNoMoreInteractions(listener); } @@ -78,7 +78,7 @@ public class DefaultMediaClockTest { @Test public void standaloneStart_shouldStartClock() throws Exception { mediaClock.start(); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -99,7 +99,7 @@ public class DefaultMediaClockTest { mediaClock.start(); mediaClock.stop(); mediaClock.start(); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -116,15 +116,14 @@ public class DefaultMediaClockTest { @Test public void standaloneSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { - PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - assertThat(parameters).isEqualTo(TEST_PLAYBACK_PARAMETERS); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @Test - public void standaloneSetPlaybackParameters_shouldTriggerCallback() { + public void standaloneSetPlaybackParameters_shouldNotTriggerCallback() { mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + verifyNoMoreInteractions(listener); } @Test @@ -132,29 +131,14 @@ public class DefaultMediaClockTest { mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); mediaClock.start(); // Asserts that clock is running with speed declared in getPlaybackParameters(). - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test public void standaloneSetOtherPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - PlaybackParameters parameters = mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT); - assertThat(parameters).isEqualTo(PlaybackParameters.DEFAULT); - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); - } - - @Test - public void standaloneSetOtherPlaybackParameters_shouldTriggerCallbackAgain() { mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT); - verify(listener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT); - } - - @Test - public void standaloneSetSamePlaybackParametersAgain_shouldTriggerCallbackAgain() { - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - verify(listener, times(2)).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); } @Test @@ -182,6 +166,7 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); } @@ -191,6 +176,7 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verifyNoMoreInteractions(listener); } @@ -200,7 +186,9 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.onRendererDisabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @@ -210,19 +198,20 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); - PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - assertThat(parameters).isEqualTo(TEST_PLAYBACK_PARAMETERS); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); } @Test - public void rendererClockSetPlaybackParameters_shouldTriggerCallback() + public void rendererClockSetPlaybackParameters_shouldNotTriggerCallback() throws ExoPlaybackException { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + verifyNoMoreInteractions(listener); } @Test @@ -231,19 +220,9 @@ public class DefaultMediaClockTest { FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ false); mediaClock.onRendererEnabled(mediaClockRenderer); - PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - assertThat(parameters).isEqualTo(PlaybackParameters.DEFAULT); - assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); - } - - @Test - public void rendererClockSetPlaybackParametersOverwrite_shouldTriggerCallback() - throws ExoPlaybackException { - FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, - /* playbackParametersAreMutable= */ false); - mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); - verify(listener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); } @Test @@ -252,7 +231,8 @@ public class DefaultMediaClockTest { mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClockRenderer.positionUs = TEST_POSITION_US; - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); // We're not advancing the renderer media clock. Thus, the clock should appear to be stopped. assertClockIsStopped(); } @@ -264,9 +244,11 @@ public class DefaultMediaClockTest { mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClockRenderer.positionUs = TEST_POSITION_US; - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); mediaClock.resetPosition(0); - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); } @Test @@ -275,23 +257,24 @@ public class DefaultMediaClockTest { mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClockRenderer.positionUs = TEST_POSITION_US; - mediaClock.syncAndGetPositionUs(); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); mediaClock.onRendererDisabled(mediaClockRenderer); fakeClock.advanceTime(SLEEP_TIME_MS); - assertThat(mediaClock.syncAndGetPositionUs()) + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) .isEqualTo(TEST_POSITION_US + C.msToUs(SLEEP_TIME_MS)); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test public void getPositionWithPlaybackParameterChange_shouldTriggerCallback() throws ExoPlaybackException { - MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, - /* playbackParametersAreMutable= */ true); + MediaClockRenderer mediaClockRenderer = + new MediaClockRenderer( + PlaybackParameters.DEFAULT, /* playbackParametersAreMutable= */ true); mediaClock.onRendererEnabled(mediaClockRenderer); // Silently change playback parameters of renderer clock. mediaClockRenderer.playbackParameters = TEST_PLAYBACK_PARAMETERS; - mediaClock.syncAndGetPositionUs(); + mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); } @@ -312,7 +295,18 @@ public class DefaultMediaClockTest { /* isEnded= */ false, /* hasReadStreamToEnd= */ true); mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); + } + + @Test + public void rendererNotReadyAndReadingAhead_shouldFallbackToStandaloneClock() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = + new MediaClockRenderer( + /* isReady= */ false, /* isEnded= */ false, /* hasReadStreamToEnd= */ false); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + assertClockIsRunning(/* isReadingAhead= */ true); } @Test @@ -322,7 +316,7 @@ public class DefaultMediaClockTest { /* isEnded= */ true, /* hasReadStreamToEnd= */ true); mediaClock.start(); mediaClock.onRendererEnabled(mediaClockRenderer); - assertClockIsRunning(); + assertClockIsRunning(/* isReadingAhead= */ false); } @Test @@ -331,7 +325,8 @@ public class DefaultMediaClockTest { MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); mediaClockRenderer.positionUs = TEST_POSITION_US; mediaClock.onRendererDisabled(mediaClockRenderer); - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(C.msToUs(fakeClock.elapsedRealtime())); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(C.msToUs(fakeClock.elapsedRealtime())); } @Test @@ -341,7 +336,8 @@ public class DefaultMediaClockTest { mediaClock.onRendererEnabled(mediaClockRenderer); mediaClock.onRendererEnabled(mediaClockRenderer); mediaClockRenderer.positionUs = TEST_POSITION_US; - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); } @Test @@ -357,20 +353,24 @@ public class DefaultMediaClockTest { } catch (ExoPlaybackException e) { // Expected. } - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(TEST_POSITION_US); } - private void assertClockIsRunning() { - long clockStartUs = mediaClock.syncAndGetPositionUs(); + private void assertClockIsRunning(boolean isReadingAhead) { + long clockStartUs = mediaClock.syncAndGetPositionUs(isReadingAhead); fakeClock.advanceTime(SLEEP_TIME_MS); - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(clockStartUs - + mediaClock.getPlaybackParameters().getMediaTimeUsForPlayoutTimeMs(SLEEP_TIME_MS)); + assertThat(mediaClock.syncAndGetPositionUs(isReadingAhead)) + .isEqualTo( + clockStartUs + + mediaClock.getPlaybackParameters().getMediaTimeUsForPlayoutTimeMs(SLEEP_TIME_MS)); } private void assertClockIsStopped() { - long positionAtStartUs = mediaClock.syncAndGetPositionUs(); + long positionAtStartUs = mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false); fakeClock.advanceTime(SLEEP_TIME_MS); - assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(positionAtStartUs); + assertThat(mediaClock.syncAndGetPositionUs(/* isReadingAhead= */ false)) + .isEqualTo(positionAtStartUs); } @SuppressWarnings("HidingField") @@ -418,11 +418,10 @@ public class DefaultMediaClockTest { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (playbackParametersAreMutable) { this.playbackParameters = playbackParameters; } - return this.playbackParameters; } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 639e80348b..f924dfb34c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.fail; import android.content.Context; import android.graphics.SurfaceTexture; +import android.net.Uri; import androidx.annotation.Nullable; import android.view.Surface; import androidx.test.core.app.ApplicationProvider; @@ -27,6 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -50,7 +52,6 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; @@ -59,6 +60,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; @@ -113,8 +115,8 @@ public final class ExoPlayerTest { /** Tests playback of a source that exposes a single period. */ @Test public void testPlaySinglePeriodTimeline() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Object manifest = new Object(); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1, manifest); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new Builder() @@ -126,7 +128,6 @@ public final class ExoPlayerTest { .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); - testRunner.assertManifestsEqual(manifest); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertThat(renderer.formatReadCount).isEqualTo(1); @@ -223,9 +224,7 @@ public final class ExoPlayerTest { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return PlaybackParameters.DEFAULT; - } + public void setPlaybackParameters(PlaybackParameters playbackParameters) {} @Override public PlaybackParameters getPlaybackParameters() { @@ -256,15 +255,16 @@ public final class ExoPlayerTest { @Test public void testRepreparationGivesFreshSourceInfo() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); Object firstSourceManifest = new Object(); - MediaSource firstSource = - new FakeMediaSource(timeline, firstSourceManifest, Builder.VIDEO_FORMAT); + Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, firstSourceManifest); + MediaSource firstSource = new FakeMediaSource(firstTimeline, Builder.VIDEO_FORMAT); final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1); final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1); + + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource secondSource = - new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) { + new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT) { @Override public synchronized void prepareSourceInternal( @Nullable TransferListener mediaTransferListener) { @@ -281,8 +281,8 @@ public final class ExoPlayerTest { } }; Object thirdSourceManifest = new Object(); - MediaSource thirdSource = - new FakeMediaSource(timeline, thirdSourceManifest, Builder.VIDEO_FORMAT); + Timeline thirdTimeline = new FakeTimeline(/* windowCount= */ 1, thirdSourceManifest); + MediaSource thirdSource = new FakeMediaSource(thirdTimeline, Builder.VIDEO_FORMAT); // Prepare the player with a source with the first manifest and a non-empty timeline. Prepare // the player again with a source and a new manifest, which will never be exposed. Allow the @@ -290,7 +290,7 @@ public final class ExoPlayerTest { // the test thread's call to prepare() has returned. ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparation") - .waitForTimelineChanged(timeline) + .waitForTimelineChanged(firstTimeline) .prepareSource(secondSource) .executeRunnable( () -> { @@ -315,8 +315,7 @@ public final class ExoPlayerTest { // The first source's preparation completed with a non-empty timeline. When the player was // re-prepared with the second source, it immediately exposed an empty timeline, but the source // info refresh from the second source was suppressed as we re-prepared with the third source. - testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); - testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest); + testRunner.assertTimelinesEqual(firstTimeline, Timeline.EMPTY, thirdTimeline); testRunner.assertTimelineChangeReasonsEqual( Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_RESET, @@ -376,9 +375,9 @@ public final class ExoPlayerTest { public void testShuffleModeEnabledChanges() throws Exception { Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource[] fakeMediaSources = { - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) }; ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false, new FakeShuffleOrder(3), fakeMediaSources); @@ -437,8 +436,7 @@ public final class ExoPlayerTest { /* isDynamic= */ false, /* durationUs= */ C.MICROS_PER_SECOND, errorAdPlaybackState)); - final FakeMediaSource fakeMediaSource = - new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testAdGroupWithLoadErrorIsSkipped") .pause() @@ -585,7 +583,7 @@ public final class ExoPlayerTest { public void testSeekDiscontinuityWithAdjustment() throws Exception { FakeTimeline timeline = new FakeTimeline(1); FakeMediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -620,7 +618,7 @@ public final class ExoPlayerTest { public void testInternalDiscontinuityAtNewPosition() throws Exception { FakeTimeline timeline = new FakeTimeline(1); FakeMediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -646,7 +644,7 @@ public final class ExoPlayerTest { public void testInternalDiscontinuityAtInitialPosition() throws Exception { FakeTimeline timeline = new FakeTimeline(1); FakeMediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -673,7 +671,7 @@ public final class ExoPlayerTest { public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); FakeTrackSelector trackSelector = new FakeTrackSelector(); @@ -702,7 +700,7 @@ public final class ExoPlayerTest { public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 2); MediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); FakeTrackSelector trackSelector = new FakeTrackSelector(); @@ -732,7 +730,7 @@ public final class ExoPlayerTest { throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); final FakeTrackSelector trackSelector = new FakeTrackSelector(); @@ -771,7 +769,7 @@ public final class ExoPlayerTest { throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = - new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + new FakeMediaSource(timeline, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); final FakeTrackSelector trackSelector = new FakeTrackSelector(/* reuse track selection */ true); @@ -810,7 +808,7 @@ public final class ExoPlayerTest { public void testDynamicTimelineChangeReason() throws Exception { Timeline timeline1 = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testDynamicTimelineChangeReason") .pause() @@ -841,14 +839,14 @@ public final class ExoPlayerTest { new ConcatenatingMediaSource( /* isAtomic= */ false, new FakeShuffleOrder(/* length= */ 2), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT)); + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); ConcatenatingMediaSource secondMediaSource = new ConcatenatingMediaSource( /* isAtomic= */ false, new FakeShuffleOrder(/* length= */ 2), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT)); + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT)); ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparationWithShuffle") // Wait for first preparation and enable shuffling. Plays period 0. @@ -877,7 +875,7 @@ public final class ExoPlayerTest { final CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1); final FakeMediaPeriod[] fakeMediaPeriodHolder = new FakeMediaPeriod[1]; MediaSource mediaSource = - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -1017,8 +1015,7 @@ public final class ExoPlayerTest { @Test public void testStopWithoutResetReleasesMediaSource() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource = - new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource") .waitForPlaybackState(Player.STATE_READY) @@ -1038,8 +1035,7 @@ public final class ExoPlayerTest { @Test public void testStopWithResetReleasesMediaSource() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource = - new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource") .waitForPlaybackState(Player.STATE_READY) @@ -1059,7 +1055,7 @@ public final class ExoPlayerTest { @Test public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - MediaSource secondSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + MediaSource secondSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparationAfterStop") .waitForPlaybackState(Player.STATE_READY) @@ -1087,7 +1083,7 @@ public final class ExoPlayerTest { public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); - MediaSource secondSource = new FakeMediaSource(secondTimeline, null, Builder.VIDEO_FORMAT); + MediaSource secondSource = new FakeMediaSource(secondTimeline, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekAfterStopWithReset") .waitForPlaybackState(Player.STATE_READY) @@ -1122,7 +1118,7 @@ public final class ExoPlayerTest { Timeline secondTimeline = new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ new Object())); - MediaSource secondSource = new FakeMediaSource(secondTimeline, /* manifest= */ null); + MediaSource secondSource = new FakeMediaSource(secondTimeline); AtomicLong positionAfterReprepare = new AtomicLong(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testReprepareAndKeepPositionWithNewMediaSource") @@ -1211,9 +1207,7 @@ public final class ExoPlayerTest { .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) .prepareSource( - new FakeMediaSource(timeline, /* manifest= */ null), - /* resetPosition= */ true, - /* resetState= */ false) + new FakeMediaSource(timeline), /* resetPosition= */ true, /* resetState= */ false) .waitForPlaybackState(Player.STATE_READY) .build(); ExoPlayerTestRunner testRunner = @@ -1252,9 +1246,7 @@ public final class ExoPlayerTest { } }) .prepareSource( - new FakeMediaSource(timeline, /* manifest= */ null), - /* resetPosition= */ false, - /* resetState= */ false) + new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ false) .waitForPlaybackState(Player.STATE_READY) .executeRunnable( new PlayerRunnable() { @@ -1287,8 +1279,7 @@ public final class ExoPlayerTest { @Test public void testInvalidSeekPositionAfterSourceInfoRefreshStillUpdatesTimeline() throws Exception { final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + final FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); ActionSchedule actionSchedule = new ActionSchedule.Builder("testInvalidSeekPositionSourceInfoRefreshStillUpdatesTimeline") .waitForPlaybackState(Player.STATE_BUFFERING) @@ -1312,8 +1303,7 @@ public final class ExoPlayerTest { public void testInvalidSeekPositionAfterSourceInfoRefreshWithShuffleModeEnabledUsesCorrectFirstPeriod() throws Exception { - FakeMediaSource mediaSource = - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null); + FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource( /* isAtomic= */ false, new FakeShuffleOrder(0), mediaSource, mediaSource); @@ -1347,7 +1337,7 @@ public final class ExoPlayerTest { public void testRestartAfterEmptyTimelineWithShuffleModeEnabledUsesCorrectFirstPeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + FakeMediaSource mediaSource = new FakeMediaSource(timeline); ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); AtomicInteger windowIndexAfterAddingSources = new AtomicInteger(); @@ -1386,8 +1376,7 @@ public final class ExoPlayerTest { final Timeline timeline = new FakeTimeline(/* windowCount= */ 2); final long[] positionHolder = new long[3]; final int[] windowIndexHolder = new int[3]; - final FakeMediaSource secondMediaSource = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + final FakeMediaSource secondMediaSource = new FakeMediaSource(/* timeline= */ null); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") .pause() @@ -1447,11 +1436,50 @@ public final class ExoPlayerTest { assertThat(windowIndexHolder[2]).isEqualTo(1); } + @Test + public void playbackErrorAndReprepareWithPositionResetKeepsWindowSequenceNumber() + throws Exception { + FakeMediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("playbackErrorWithResetKeepsWindowSequenceNumber") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) + .waitForPlaybackState(Player.STATE_IDLE) + .prepareSource(mediaSource, /* resetPosition= */ true, /* resetState= */ false) + .waitForPlaybackState(Player.STATE_READY) + .play() + .build(); + HashSet reportedWindowSequenceNumbers = new HashSet<>(); + AnalyticsListener listener = + new AnalyticsListener() { + @Override + public void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, int playbackState) { + if (eventTime.mediaPeriodId != null) { + reportedWindowSequenceNumbers.add(eventTime.mediaPeriodId.windowSequenceNumber); + } + } + }; + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .setAnalyticsListener(listener) + .build(context); + try { + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + } + assertThat(reportedWindowSequenceNumbers).hasSize(1); + } + @Test public void testPlaybackErrorTwiceStillKeepsTimeline() throws Exception { final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - final FakeMediaSource mediaSource2 = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + final FakeMediaSource mediaSource2 = new FakeMediaSource(/* timeline= */ null); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlaybackErrorDoesNotResetPosition") .pause() @@ -1609,9 +1637,7 @@ public final class ExoPlayerTest { // messages sent at end of playback are received before test ends. .waitForPlaybackState(Player.STATE_ENDED) .prepareSource( - new FakeMediaSource(timeline, null), - /* resetPosition= */ false, - /* resetState= */ true) + new FakeMediaSource(timeline), /* resetPosition= */ false, /* resetState= */ true) .waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_ENDED) .build(); @@ -1774,7 +1800,7 @@ public final class ExoPlayerTest { new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") @@ -1847,7 +1873,7 @@ public final class ExoPlayerTest { new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, Builder.VIDEO_FORMAT); PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessages") @@ -1873,9 +1899,9 @@ public final class ExoPlayerTest { public void testSendMessagesNonLinearPeriodOrder() throws Exception { Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource[] fakeMediaSources = { - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) }; ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false, new FakeShuffleOrder(3), fakeMediaSources); @@ -2022,8 +2048,7 @@ public final class ExoPlayerTest { new FakeTimeline( new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3)); - final FakeMediaSource mediaSource = - new FakeMediaSource(timeline1, /* manifest= */ null, Builder.VIDEO_FORMAT); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testTimelineUpdateDropsPeriods") .pause() @@ -2069,7 +2094,7 @@ public final class ExoPlayerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - FakeMediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + FakeMediaSource mediaSource = new FakeMediaSource(timeline); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekToUnpreparedPeriod") .pause() @@ -2163,7 +2188,7 @@ public final class ExoPlayerTest { final EventListener eventListener = new EventListener() { @Override - public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { + public void onTimelineChanged(Timeline timeline, int reason) { if (timeline.isEmpty()) { playerReference.get().setPlayWhenReady(/* playWhenReady= */ false); } @@ -2208,7 +2233,7 @@ public final class ExoPlayerTest { long expectedDurationUs = 700_000; MediaSource mediaSource = new ClippingMediaSource( - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null), + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), startPositionUs, startPositionUs + expectedDurationUs); Clock clock = new AutoAdvancingFakeClock(); @@ -2269,17 +2294,15 @@ public final class ExoPlayerTest { public void testUpdateTrackSelectorThenSeekToUnpreparedPeriod_returnsEmptyTrackGroups() throws Exception { // Use unset duration to prevent pre-loading of the second window. - Timeline fakeTimeline = + Timeline timelineUnsetDuration = new FakeTimeline( new TimelineWindowDefinition( /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ C.TIME_UNSET)); - MediaSource[] fakeMediaSources = { - new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), - new FakeMediaSource(fakeTimeline, null, Builder.AUDIO_FORMAT) - }; - MediaSource mediaSource = new ConcatenatingMediaSource(fakeMediaSources); - FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); - DefaultTrackSelector trackSelector = new DefaultTrackSelector(); + Timeline timelineSetDuration = new FakeTimeline(/* windowCount= */ 1); + MediaSource mediaSource = + new ConcatenatingMediaSource( + new FakeMediaSource(timelineUnsetDuration, Builder.VIDEO_FORMAT), + new FakeMediaSource(timelineSetDuration, Builder.AUDIO_FORMAT)); ActionSchedule actionSchedule = new ActionSchedule.Builder("testUpdateTrackSelectorThenSeekToUnpreparedPeriod") .pause() @@ -2291,8 +2314,7 @@ public final class ExoPlayerTest { List trackSelectionsList = new ArrayList<>(); new Builder() .setMediaSource(mediaSource) - .setTrackSelector(trackSelector) - .setRenderers(renderer) + .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) .setActionSchedule(actionSchedule) .setEventListener( new EventListener() { @@ -2326,10 +2348,9 @@ public final class ExoPlayerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - MediaSource workingMediaSource = - new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + MediaSource workingMediaSource = new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT); MediaSource failingMediaSource = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(/* timeline= */ null, Builder.VIDEO_FORMAT) { @Override public void maybeThrowSourceInfoRefreshError() throws IOException { throw new IOException(); @@ -2363,10 +2384,9 @@ public final class ExoPlayerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10 * C.MICROS_PER_SECOND)); - MediaSource workingMediaSource = - new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + MediaSource workingMediaSource = new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT); MediaSource failingMediaSource = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(/* timeline= */ null, Builder.VIDEO_FORMAT) { @Override public void maybeThrowSourceInfoRefreshError() throws IOException { throw new IOException(); @@ -2408,7 +2428,7 @@ public final class ExoPlayerTest { /* durationUs= */ 10 * C.MICROS_PER_SECOND)); AtomicReference wasReadyOnce = new AtomicReference<>(false); MediaSource mediaSource = - new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT) { + new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT) { @Override public void maybeThrowSourceInfoRefreshError() throws IOException { if (wasReadyOnce.get()) { @@ -2446,7 +2466,7 @@ public final class ExoPlayerTest { new FakeTimeline( new TimelineWindowDefinition( /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 100_000)); - MediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + MediaSource mediaSource = new FakeMediaSource(timeline); ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(mediaSource); ActionSchedule actionSchedule = new ActionSchedule.Builder("removingLoopingLastPeriodFromPlaylistDoesNotThrow") @@ -2471,7 +2491,7 @@ public final class ExoPlayerTest { public void seekToUnpreparedWindowWithNonZeroOffsetInConcatenationStartsAtCorrectPosition() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); MediaSource clippedMediaSource = new ClippingMediaSource( mediaSource, @@ -2519,7 +2539,7 @@ public final class ExoPlayerTest { /* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 2 * periodDurationMs * 1000)); - FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null); MediaSource concatenatedMediaSource = new ConcatenatingMediaSource(mediaSource); AtomicInteger periodIndexWhenReady = new AtomicInteger(); AtomicLong positionWhenReady = new AtomicLong(); @@ -2607,6 +2627,145 @@ public final class ExoPlayerTest { assertThat(bufferedPositionAtFirstDiscontinuityMs.get()).isEqualTo(C.usToMs(windowDurationUs)); } + @Test + public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() throws Exception { + AdPlaybackState adPlaybackState = + FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs= */ 0) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.parse("https://ad1")) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, Uri.parse("https://ad2")) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, Uri.parse("https://ad3")); + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10_000_000, + adPlaybackState)); + final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline); + AtomicReference playerReference = new AtomicReference<>(); + AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET); + EventListener eventListener = + new EventListener() { + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) { + contentStartPositionMs.set(playerReference.get().getContentPosition()); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("contentWithInitialSeekAfterPrerollAd") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener); + } + }) + .seek(5_000) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(fakeMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(contentStartPositionMs.get()).isAtLeast(5_000L); + } + + @Test + public void setPlaybackParametersConsecutivelyNotifiesListenerForEveryChangeOnce() + throws Exception { + ActionSchedule actionSchedule = + new ActionSchedule.Builder("setPlaybackParametersNotifiesListenerForEveryChangeOnce") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .setPlaybackParameters(new PlaybackParameters(1.1f)) + .setPlaybackParameters(new PlaybackParameters(1.2f)) + .setPlaybackParameters(new PlaybackParameters(1.3f)) + .play() + .build(); + List reportedPlaybackParameters = new ArrayList<>(); + EventListener listener = + new EventListener() { + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + reportedPlaybackParameters.add(playbackParameters); + } + }; + new ExoPlayerTestRunner.Builder() + .setActionSchedule(actionSchedule) + .setEventListener(listener) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(reportedPlaybackParameters) + .containsExactly( + new PlaybackParameters(1.1f), + new PlaybackParameters(1.2f), + new PlaybackParameters(1.3f)) + .inOrder(); + } + + @Test + public void + setUnsupportedPlaybackParametersConsecutivelyNotifiesListenerForEveryChangeOnceAndResetsOnceHandled() + throws Exception { + Renderer renderer = + new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) { + @Override + public long getPositionUs() { + return 0; + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) {} + + @Override + public PlaybackParameters getPlaybackParameters() { + return PlaybackParameters.DEFAULT; + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("setUnsupportedPlaybackParametersNotifiesListenersCorrectly") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .setPlaybackParameters(new PlaybackParameters(1.1f)) + .setPlaybackParameters(new PlaybackParameters(1.2f)) + .setPlaybackParameters(new PlaybackParameters(1.3f)) + .play() + .build(); + List reportedPlaybackParameters = new ArrayList<>(); + EventListener listener = + new EventListener() { + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + reportedPlaybackParameters.add(playbackParameters); + } + }; + new ExoPlayerTestRunner.Builder() + .setSupportedFormats(Builder.AUDIO_FORMAT) + .setRenderers(renderer) + .setActionSchedule(actionSchedule) + .setEventListener(listener) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(reportedPlaybackParameters) + .containsExactly( + new PlaybackParameters(1.1f), + new PlaybackParameters(1.2f), + new PlaybackParameters(1.3f), + PlaybackParameters.DEFAULT) + .inOrder(); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java index 96bb606eae..fe2a8c7d4b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java @@ -91,7 +91,8 @@ public final class FormatTest { /* encoderDelay= */ 1001, /* encoderPadding= */ 1002, "language", - /* accessibilityChannel= */ Format.NO_VALUE); + /* accessibilityChannel= */ Format.NO_VALUE, + /* exoMediaCryptoType= */ null); Parcel parcel = Parcel.obtain(); formatToParcel.writeToParcel(parcel, 0); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 73f42c5fc9..14aa436be3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -26,7 +26,9 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; +import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; import org.junit.Before; import org.junit.Test; @@ -353,7 +355,6 @@ public final class MediaPeriodQueueTest { playbackInfo = new PlaybackInfo( timeline, - /* manifest= */ null, mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs), /* startPositionUs= */ 0, /* contentPositionUs= */ 0, @@ -382,7 +383,13 @@ public final class MediaPeriodQueueTest { private void enqueueNext() { mediaPeriodQueue.enqueueNextMediaPeriod( - rendererCapabilities, trackSelector, allocator, mediaSource, getNextMediaPeriodInfo()); + rendererCapabilities, + trackSelector, + allocator, + mediaSource, + getNextMediaPeriodInfo(), + new TrackSelectorResult( + new RendererConfiguration[0], new TrackSelection[0], /* info= */ null)); } private MediaPeriodInfo getNextMediaPeriodInfo() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 2e9b539096..875f8b5d7b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -125,7 +125,9 @@ public final class AnalyticsCollectorTest { public void testEmptyTimeline() throws Exception { FakeMediaSource mediaSource = new FakeMediaSource( - Timeline.EMPTY, /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + Timeline.EMPTY, + ExoPlayerTestRunner.Builder.VIDEO_FORMAT, + ExoPlayerTestRunner.Builder.AUDIO_FORMAT); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) @@ -140,7 +142,6 @@ public final class AnalyticsCollectorTest { FakeMediaSource mediaSource = new FakeMediaSource( SINGLE_PERIOD_TIMELINE, - /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); @@ -183,12 +184,10 @@ public final class AnalyticsCollectorTest { new ConcatenatingMediaSource( new FakeMediaSource( SINGLE_PERIOD_TIMELINE, - /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT), new FakeMediaSource( SINGLE_PERIOD_TIMELINE, - /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); @@ -242,9 +241,8 @@ public final class AnalyticsCollectorTest { public void testPeriodTransitionWithRendererChange() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), - new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.AUDIO_FORMAT)); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); @@ -296,9 +294,8 @@ public final class AnalyticsCollectorTest { public void testSeekToOtherPeriod() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), - new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.AUDIO_FORMAT)); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT)); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .pause() @@ -361,12 +358,9 @@ public final class AnalyticsCollectorTest { public void testSeekBackAfterReadingAhead() throws Exception { MediaSource mediaSource = new ConcatenatingMediaSource( - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT), new FakeMediaSource( - SINGLE_PERIOD_TIMELINE, - /* manifest= */ null, - Builder.VIDEO_FORMAT, - Builder.AUDIO_FORMAT)); + SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)); long periodDurationMs = SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); ActionSchedule actionSchedule = @@ -374,8 +368,7 @@ public final class AnalyticsCollectorTest { .pause() .waitForPlaybackState(Player.STATE_READY) .playUntilPosition(/* windowIndex= */ 0, periodDurationMs) - .seek(/* positionMs= */ 0) - .waitForPlaybackState(Player.STATE_READY) + .seekAndWait(/* positionMs= */ 0) .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); @@ -384,8 +377,8 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, - WINDOW_0 /* BUFFERING */, WINDOW_0 /* setPlayWhenReady=false */, + WINDOW_0 /* BUFFERING */, period0 /* READY */, period0 /* setPlayWhenReady=true */, period0 /* setPlayWhenReady=false */, @@ -443,10 +436,8 @@ public final class AnalyticsCollectorTest { @Test public void testPrepareNewSource() throws Exception { - MediaSource mediaSource1 = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); - MediaSource mediaSource2 = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + MediaSource mediaSource1 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); + MediaSource mediaSource2 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .pause() @@ -507,13 +498,13 @@ public final class AnalyticsCollectorTest { @Test public void testReprepareAfterError() throws Exception { - MediaSource mediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) + .seek(/* positionMs= */ 0) .prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false) .waitForPlaybackState(Player.STATE_ENDED) .build(); @@ -531,6 +522,9 @@ public final class AnalyticsCollectorTest { period0Seq0 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(WINDOW_0); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period0Seq0); @@ -570,7 +564,7 @@ public final class AnalyticsCollectorTest { @Test public void testDynamicTimelineChange() throws Exception { MediaSource childMediaSource = - new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT); final ConcatenatingMediaSource concatenatedMediaSource = new ConcatenatingMediaSource(childMediaSource, childMediaSource); long periodDurationMs = @@ -594,8 +588,8 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, - WINDOW_0 /* BUFFERING */, WINDOW_0 /* setPlayWhenReady=false */, + WINDOW_0 /* BUFFERING */, window0Period1Seq0 /* READY */, window0Period1Seq0 /* setPlayWhenReady=true */, window0Period1Seq0 /* setPlayWhenReady=false */, @@ -639,7 +633,7 @@ public final class AnalyticsCollectorTest { @Test public void testNotifyExternalEvents() throws Exception { - MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null); + MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") .pause() @@ -847,7 +841,7 @@ public final class AnalyticsCollectorTest { private static final class EventWindowAndPeriodId { private final int windowIndex; - private final @Nullable MediaPeriodId mediaPeriodId; + @Nullable private final MediaPeriodId mediaPeriodId; public EventWindowAndPeriodId(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { this.windowIndex = windowIndex; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/AudioFocusManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/AudioFocusManagerTest.java index 272c944e46..544975ea03 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/AudioFocusManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/AudioFocusManagerTest.java @@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.audio.AudioFocusManager.PLAYER_COMMA import static com.google.android.exoplayer2.audio.AudioFocusManager.PLAYER_COMMAND_WAIT_FOR_CALLBACK; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.robolectric.annotation.Config.TARGET_SDK; import android.content.Context; import android.media.AudioFocusRequest; @@ -99,7 +100,7 @@ public class AudioFocusManagerTest { } @Test - @Config(minSdk = 26) + @Config(minSdk = 26, maxSdk = TARGET_SDK) public void setAudioAttributes_withNullUsage_releasesAudioFocus_v26() { // Create attributes and request audio focus. AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); @@ -301,7 +302,7 @@ public class AudioFocusManagerTest { } @Test - @Config(minSdk = 26) + @Config(minSdk = 26, maxSdk = TARGET_SDK) public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocus_v26() { // Ensure that AUDIOFOCUS_LOSS causes AudioFocusManager to pause playback and abandon audio // focus. @@ -351,7 +352,7 @@ public class AudioFocusManagerTest { } @Test - @Config(minSdk = 26) + @Config(minSdk = 26, maxSdk = TARGET_SDK) public void handleStop_withAudioFocus_abandonsAudioFocus_v26() { // Ensure that handleStop causes AudioFocusManager to abandon audio focus. AudioAttributes media = @@ -421,7 +422,7 @@ public class AudioFocusManagerTest { } @Test - @Config(minSdk = 26) + @Config(minSdk = 26, maxSdk = TARGET_SDK) public void handleStop_withoutHandlingAudioFocus_isNoOp_v26() { // Ensure that handleStop is a no-op if audio focus isn't handled. Shadows.shadowOf(audioManager) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java index d41c99183d..c0b5205455 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.audio; import static com.google.common.truth.Truth.assertThat; -import static org.robolectric.annotation.Config.NEWEST_SDK; import static org.robolectric.annotation.Config.OLDEST_SDK; +import static org.robolectric.annotation.Config.TARGET_SDK; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -185,7 +185,7 @@ public final class DefaultAudioSinkTest { .isFalse(); } - @Config(minSdk = 21, maxSdk = NEWEST_SDK) + @Config(minSdk = 21, maxSdk = TARGET_SDK) @Test public void supportsFloatOutputFromApi21() { assertThat(defaultAudioSink.supportsOutput(CHANNEL_COUNT_STEREO, C.ENCODING_PCM_FLOAT)) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java index 950061e9bc..6769f5049b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -54,20 +55,22 @@ public class SimpleDecoderAudioRendererTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - audioRenderer = new SimpleDecoderAudioRenderer(null, null, null, false, mockAudioSink) { - @Override - protected int supportsFormatInternal(DrmSessionManager drmSessionManager, - Format format) { - return FORMAT_HANDLED; - } + audioRenderer = + new SimpleDecoderAudioRenderer(null, null, null, false, mockAudioSink) { + @Override + protected int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format) { + return FORMAT_HANDLED; + } - @Override - protected SimpleDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) - throws AudioDecoderException { - return new FakeDecoder(); - } - }; + @Override + protected SimpleDecoder< + DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends AudioDecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws AudioDecoderException { + return new FakeDecoder(); + } + }; } @Config(sdk = 19) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 83ca752114..c371389483 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.drm; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.when; import android.util.Pair; @@ -46,6 +47,9 @@ public class OfflineLicenseHelperTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); + when(mediaDrm.getKeyRequest(any(), any(), anyInt(), any())) + .thenReturn( + new ExoMediaDrm.KeyRequest(/* data= */ new byte[0], /* licenseServerUrl= */ "")); offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, null); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java index be9100cb9d..43ff7d8f51 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java @@ -45,13 +45,13 @@ public final class DefaultExtractorsFactoryTest { DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); Extractor[] extractors = defaultExtractorsFactory.createExtractors(); - List listCreatedExtractorClasses = new ArrayList<>(); + List> listCreatedExtractorClasses = new ArrayList<>(); for (Extractor extractor : extractors) { listCreatedExtractorClasses.add(extractor.getClass()); } - Class[] expectedExtractorClassses = - new Class[] { + Class[] expectedExtractorClassses = + new Class[] { MatroskaExtractor.class, FragmentedMp4Extractor.class, Mp4Extractor.class, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index 8d1818845d..8ba0be26a0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -16,13 +16,16 @@ package com.google.android.exoplayer2.extractor.ogg; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.OggTestData; +import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; import java.io.IOException; import java.util.Random; import org.junit.Test; @@ -32,13 +35,15 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class DefaultOggSeekerTest { + private final Random random = new Random(0); + @Test public void testSetupWithUnsetEndPositionFails() { try { new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ C.LENGTH_UNSET, /* streamReader= */ new TestStreamReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ C.LENGTH_UNSET, /* firstPayloadPageSize= */ 1, /* firstPayloadPageGranulePosition= */ 1, /* firstPayloadPageIsLastPage= */ false); @@ -56,17 +61,106 @@ public final class DefaultOggSeekerTest { } } + @Test + public void testSkipToNextPage() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(4000, random), + new byte[] {'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random)), + false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(4000); + } + + @Test + public void testSkipToNextPageOverlap() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(2046, random), + new byte[] {'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random)), + false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(2046); + } + + @Test + public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays(new byte[] {'x', 'O', 'g', 'g', 'S'}), false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(1); + } + + @Test + public void testSkipToNextPageNoMatch() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput(new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); + try { + skipToNextPage(extractorInput); + fail(); + } catch (EOFException e) { + // expected + } + } + + @Test + public void testReadGranuleOfLastPage() throws IOException, InterruptedException { + FakeExtractorInput input = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(100, random), + OggTestData.buildOggHeader(0x00, 20000, 66, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random), + OggTestData.buildOggHeader(0x00, 40000, 67, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random), + OggTestData.buildOggHeader(0x05, 60000, 68, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random)), + false); + assertReadGranuleOfLastPage(input, 60000); + } + + @Test + public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { + FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (EOFException e) { + // Ignored. + } + } + + @Test + public void testReadGranuleOfLastPageWithUnboundedLength() + throws IOException, InterruptedException { + FakeExtractorInput input = OggTestData.createInput(new byte[0], true); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (IllegalArgumentException e) { + // Ignored. + } + } + private void testSeeking(Random random) throws IOException, InterruptedException { OggTestFile testFile = OggTestFile.generate(random, 1000); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build(); TestStreamReader streamReader = new TestStreamReader(); DefaultOggSeeker oggSeeker = new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ testFile.data.length, /* streamReader= */ streamReader, + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ testFile.data.length, /* firstPayloadPageSize= */ testFile.firstPayloadPageSize, - /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition, + /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranuleCount, /* firstPayloadPageIsLastPage= */ false); OggPageHeader pageHeader = new OggPageHeader(); @@ -78,89 +172,96 @@ public final class DefaultOggSeekerTest { input.setPosition((int) nextSeekPosition); } - // Test granule 0 from file start - assertThat(seekTo(input, oggSeeker, 0, 0)).isEqualTo(0); + // Test granule 0 from file start. + long granule = seekTo(input, oggSeeker, 0, 0); + assertThat(granule).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0); - // Test granule 0 from file end - assertThat(seekTo(input, oggSeeker, 0, testFile.data.length - 1)).isEqualTo(0); + // Test granule 0 from file end. + granule = seekTo(input, oggSeeker, 0, testFile.data.length - 1); + assertThat(granule).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0); - { // Test last granule - long currentGranule = seekTo(input, oggSeeker, testFile.lastGranule, 0); - long position = testFile.data.length; - assertThat( - (testFile.lastGranule > currentGranule && position > input.getPosition()) - || (testFile.lastGranule == currentGranule && position == input.getPosition())) - .isTrue(); - } - - { // Test exact granule - input.setPosition(testFile.data.length / 2); - oggSeeker.skipToNextPage(input); - assertThat(pageHeader.populate(input, true)).isTrue(); - long position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize; - long currentGranule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0); - assertThat( - (pageHeader.granulePosition > currentGranule && position > input.getPosition()) - || (pageHeader.granulePosition == currentGranule - && position == input.getPosition())) - .isTrue(); - } + // Test last granule. + granule = seekTo(input, oggSeeker, testFile.granuleCount - 1, 0); + assertThat(granule).isEqualTo(testFile.granuleCount - testFile.lastPayloadPageGranuleCount); + assertThat(input.getPosition()).isEqualTo(testFile.data.length - testFile.lastPayloadPageSize); for (int i = 0; i < 100; i += 1) { - long targetGranule = (long) (random.nextDouble() * testFile.lastGranule); + long targetGranule = random.nextInt(testFile.granuleCount); int initialPosition = random.nextInt(testFile.data.length); - - long currentGranule = seekTo(input, oggSeeker, targetGranule, initialPosition); + granule = seekTo(input, oggSeeker, targetGranule, initialPosition); long currentPosition = input.getPosition(); - - assertWithMessage("getNextSeekPosition() didn't leave input on a page start.") - .that(pageHeader.populate(input, true)) - .isTrue(); - - if (currentGranule == 0) { + if (granule == 0) { assertThat(currentPosition).isEqualTo(0); } else { int previousPageStart = testFile.findPreviousPageStart(currentPosition); input.setPosition(previousPageStart); - assertThat(pageHeader.populate(input, true)).isTrue(); - assertThat(currentGranule).isEqualTo(pageHeader.granulePosition); + pageHeader.populate(input, false); + assertThat(granule).isEqualTo(pageHeader.granulePosition); } input.setPosition((int) currentPosition); - oggSeeker.skipToPageOfGranule(input, targetGranule, -1); - long positionDiff = Math.abs(input.getPosition() - currentPosition); + pageHeader.populate(input, false); + // The target granule should be within the current page. + assertThat(granule).isAtMost(targetGranule); + assertThat(targetGranule).isLessThan(pageHeader.granulePosition); + } + } - long granuleDiff = currentGranule - targetGranule; - if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0) - && positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) { - fail( - "granuleDiff (" - + granuleDiff - + ") or positionDiff (" - + positionDiff - + ") is more than allowed."); + private static void skipToNextPage(ExtractorInput extractorInput) + throws IOException, InterruptedException { + DefaultOggSeeker oggSeeker = + new DefaultOggSeeker( + /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ extractorInput.getLength(), + /* firstPayloadPageSize= */ 1, + /* firstPayloadPageGranulePosition= */ 2, + /* firstPayloadPageIsLastPage= */ false); + while (true) { + try { + oggSeeker.skipToNextPage(extractorInput); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { + /* ignored */ } } } - private long seekTo( + private static void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) + throws IOException, InterruptedException { + DefaultOggSeeker oggSeeker = + new DefaultOggSeeker( + /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ input.getLength(), + /* firstPayloadPageSize= */ 1, + /* firstPayloadPageGranulePosition= */ 2, + /* firstPayloadPageIsLastPage= */ false); + while (true) { + try { + assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { + // Ignored. + } + } + } + + private static long seekTo( FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition) throws IOException, InterruptedException { long nextSeekPosition = initialPosition; + oggSeeker.startSeek(targetGranule); int count = 0; - oggSeeker.resetSeeking(); - - do { - input.setPosition((int) nextSeekPosition); - nextSeekPosition = oggSeeker.getNextSeekPosition(targetGranule, input); - + while (nextSeekPosition >= 0) { if (count++ > 100) { - fail("infinite loop?"); + fail("Seek failed to converge in 100 iterations"); } - } while (nextSeekPosition >= 0); - + input.setPosition((int) nextSeekPosition); + nextSeekPosition = oggSeeker.read(input); + } return -(nextSeekPosition + 2); } @@ -171,8 +272,7 @@ public final class DefaultOggSeekerTest { } @Override - protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) - throws IOException, InterruptedException { + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { return false; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java deleted file mode 100644 index d6691f50f8..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 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.extractor.ogg; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.OggTestData; -import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.EOFException; -import java.io.IOException; -import java.util.Random; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link DefaultOggSeeker} utility methods. */ -@RunWith(AndroidJUnit4.class) -public final class DefaultOggSeekerUtilMethodsTest { - - private final Random random = new Random(0); - - @Test - public void testSkipToNextPage() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(4000, random), - new byte[] {'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(4000); - } - - @Test - public void testSkipToNextPageOverlap() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(2046, random), - new byte[] {'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(2046); - } - - @Test - public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - new byte[] {'x', 'O', 'g', 'g', 'S'} - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(1); - } - - @Test - public void testSkipToNextPageNoMatch() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); - try { - skipToNextPage(extractorInput); - fail(); - } catch (EOFException e) { - // expected - } - } - - private static void skipToNextPage(ExtractorInput extractorInput) - throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ extractorInput.getLength(), - /* streamReader= */ new FlacReader(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - oggSeeker.skipToNextPage(extractorInput); - break; - } catch (FakeExtractorInput.SimulatedIOException e) { /* ignored */ } - } - } - - @Test - public void testSkipToPageOfGranule() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - // expect to be granule of the previous page returned as elapsedSamples - skipToPageOfGranule(input, 54000, 40000); - // expect to be at the start of the third page - assertThat(input.getPosition()).isEqualTo(2 * (30 + (3 * 254))); - } - - @Test - public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - skipToPageOfGranule(input, 40000, 20000); - // expect to be at the start of the second page - assertThat(input.getPosition()).isEqualTo(30 + (3 * 254)); - } - - @Test - public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - skipToPageOfGranule(input, 10000, -1); - assertThat(input.getPosition()).isEqualTo(0); - } - - private void skipToPageOfGranule(ExtractorInput input, long granule, - long elapsedSamplesExpected) throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ input.getLength(), - /* streamReader= */ new FlacReader(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - assertThat(oggSeeker.skipToPageOfGranule(input, granule, -1)) - .isEqualTo(elapsedSamplesExpected); - return; - } catch (FakeExtractorInput.SimulatedIOException e) { - input.resetPeekPosition(); - } - } - } - - @Test - public void testReadGranuleOfLastPage() throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays( - TestUtil.buildTestData(100, random), - OggTestData.buildOggHeader(0x00, 20000, 66, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - OggTestData.buildOggHeader(0x00, 40000, 67, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - OggTestData.buildOggHeader(0x05, 60000, 68, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random) - ), false); - assertReadGranuleOfLastPage(input, 60000); - } - - @Test - public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (EOFException e) { - // ignored - } - } - - @Test - public void testReadGranuleOfLastPageWithUnboundedLength() - throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(new byte[0], true); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (IllegalArgumentException e) { - // ignored - } - } - - private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) - throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ input.getLength(), - /* streamReader= */ new FlacReader(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); - break; - } catch (FakeExtractorInput.SimulatedIOException e) { - // ignored - } - } - } - -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java index e5512dda36..38e4332b16 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java @@ -30,35 +30,39 @@ import java.util.Random; private static final int MAX_GRANULES_IN_PAGE = 100000; public final byte[] data; - public final long lastGranule; - public final int packetCount; + public final int granuleCount; public final int pageCount; public final int firstPayloadPageSize; - public final long firstPayloadPageGranulePosition; + public final int firstPayloadPageGranuleCount; + public final int lastPayloadPageSize; + public final int lastPayloadPageGranuleCount; private OggTestFile( byte[] data, - long lastGranule, - int packetCount, + int granuleCount, int pageCount, int firstPayloadPageSize, - long firstPayloadPageGranulePosition) { + int firstPayloadPageGranuleCount, + int lastPayloadPageSize, + int lastPayloadPageGranuleCount) { this.data = data; - this.lastGranule = lastGranule; - this.packetCount = packetCount; + this.granuleCount = granuleCount; this.pageCount = pageCount; this.firstPayloadPageSize = firstPayloadPageSize; - this.firstPayloadPageGranulePosition = firstPayloadPageGranulePosition; + this.firstPayloadPageGranuleCount = firstPayloadPageGranuleCount; + this.lastPayloadPageSize = lastPayloadPageSize; + this.lastPayloadPageGranuleCount = lastPayloadPageGranuleCount; } public static OggTestFile generate(Random random, int pageCount) { ArrayList fileData = new ArrayList<>(); int fileSize = 0; - long granule = 0; - int packetLength = -1; - int packetCount = 0; + int granuleCount = 0; int firstPayloadPageSize = 0; - long firstPayloadPageGranulePosition = 0; + int firstPayloadPageGranuleCount = 0; + int lastPageloadPageSize = 0; + int lastPayloadPageGranuleCount = 0; + int packetLength = -1; for (int i = 0; i < pageCount; i++) { int headerType = 0x00; @@ -71,17 +75,17 @@ import java.util.Random; if (i == pageCount - 1) { headerType |= 4; } - granule += random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; + int pageGranuleCount = random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT); - byte[] header = OggTestData.buildOggHeader(headerType, granule, 0, pageSegmentCount); + granuleCount += pageGranuleCount; + byte[] header = OggTestData.buildOggHeader(headerType, granuleCount, 0, pageSegmentCount); fileData.add(header); - fileSize += header.length; + int pageSize = header.length; byte[] laces = new byte[pageSegmentCount]; int bodySize = 0; for (int j = 0; j < pageSegmentCount; j++) { if (packetLength < 0) { - packetCount++; if (i < pageCount - 1) { packetLength = random.nextInt(MAX_PACKET_LENGTH); } else { @@ -96,14 +100,19 @@ import java.util.Random; packetLength -= 255; } fileData.add(laces); - fileSize += laces.length; + pageSize += laces.length; byte[] payload = TestUtil.buildTestData(bodySize, random); fileData.add(payload); - fileSize += payload.length; + pageSize += payload.length; + + fileSize += pageSize; if (i == 0) { - firstPayloadPageSize = header.length + bodySize; - firstPayloadPageGranulePosition = granule; + firstPayloadPageSize = pageSize; + firstPayloadPageGranuleCount = pageGranuleCount; + } else if (i == pageCount - 1) { + lastPageloadPageSize = pageSize; + lastPayloadPageGranuleCount = pageGranuleCount; } } @@ -115,11 +124,12 @@ import java.util.Random; } return new OggTestFile( file, - granule, - packetCount, + granuleCount, pageCount, firstPayloadPageSize, - firstPayloadPageGranulePosition); + firstPayloadPageGranuleCount, + lastPageloadPageSize, + lastPayloadPageGranuleCount); } public int findPreviousPageStart(long position) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java index 4527e41f34..060f7fb81d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java @@ -217,9 +217,7 @@ public final class AdtsExtractorSeekTest { // Internal methods private static AdtsExtractor createAdtsExtractor() { - return new AdtsExtractor( - /* firstStreamSampleTimestampUs= */ 0, - /* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); + return new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING); } private void assertFirstSampleAfterSeekContainTargetSeekTime( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java index 25e2a336ff..feb14d1adb 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java @@ -32,10 +32,7 @@ public final class AdtsExtractorTest { @Test public void testSample_withSeeking() throws Exception { ExtractorAsserts.assertBehavior( - () -> - new AdtsExtractor( - /* firstStreamSampleTimestampUs= */ 0, - /* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), + () -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), "ts/sample_cbs.adts"); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java index a84c6f5d7b..e8d65255c3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtilTest.java @@ -20,6 +20,10 @@ import static com.google.common.truth.Truth.assertThat; import android.media.MediaCodecInfo; import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.video.ColorInfo; import org.junit.Test; import org.junit.runner.RunWith; @@ -60,19 +64,146 @@ public final class MediaCodecUtilTest { MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd60); } + @Test + public void getCodecProfileAndLevel_handlesAv1ProfileMain8CodecString() { + assertCodecProfileAndLevelForCodecsString( + "av01.0.10M.08", + MediaCodecInfo.CodecProfileLevel.AV1ProfileMain8, + MediaCodecInfo.CodecProfileLevel.AV1Level42); + } + + @Test + public void getCodecProfileAndLevel_handlesAv1ProfileMain10CodecString() { + assertCodecProfileAndLevelForCodecsString( + "av01.0.20M.10", + MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10, + MediaCodecInfo.CodecProfileLevel.AV1Level7); + } + + @Test + public void getCodecProfileAndLevel_handlesAv1ProfileMain10HDRWithHdrInfoSet() { + ColorInfo colorInfo = + new ColorInfo( + /* colorSpace= */ C.COLOR_SPACE_BT709, + /* colorRange= */ C.COLOR_RANGE_LIMITED, + /* colorTransfer= */ C.COLOR_TRANSFER_SDR, + /* hdrStaticInfo= */ new byte[] {1, 2, 3, 4, 5, 6, 7}); + Format format = + Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, + /* codecs= */ "av01.0.21M.10", + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ 1024, + /* height= */ 768, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* rotationDegrees= */ Format.NO_VALUE, + /* pixelWidthHeightRatio= */ 0, + /* projectionData= */ null, + /* stereoMode= */ Format.NO_VALUE, + /* colorInfo= */ colorInfo, + /* drmInitData */ null); + assertCodecProfileAndLevelForFormat( + format, + MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10, + MediaCodecInfo.CodecProfileLevel.AV1Level71); + } + + @Test + public void getCodecProfileAndLevel_handlesAv1ProfileMain10HDRWithoutHdrInfoSet() { + ColorInfo colorInfo = + new ColorInfo( + /* colorSpace= */ C.COLOR_SPACE_BT709, + /* colorRange= */ C.COLOR_RANGE_LIMITED, + /* colorTransfer= */ C.COLOR_TRANSFER_HLG, + /* hdrStaticInfo= */ null); + Format format = + Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, + /* codecs= */ "av01.0.21M.10", + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ 1024, + /* height= */ 768, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* rotationDegrees= */ Format.NO_VALUE, + /* pixelWidthHeightRatio= */ 0, + /* projectionData= */ null, + /* stereoMode= */ Format.NO_VALUE, + /* colorInfo= */ colorInfo, + /* drmInitData */ null); + assertCodecProfileAndLevelForFormat( + format, + MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10HDR10, + MediaCodecInfo.CodecProfileLevel.AV1Level71); + } + + @Test + public void getCodecProfileAndLevel_handlesFullAv1CodecString() { + // Example from https://aomediacodec.github.io/av1-isobmff/#codecsparam. + assertCodecProfileAndLevelForCodecsString( + "av01.0.04M.10.0.112.09.16.09.0", + MediaCodecInfo.CodecProfileLevel.AV1ProfileMain10, + MediaCodecInfo.CodecProfileLevel.AV1Level3); + } + @Test public void getCodecProfileAndLevel_rejectsNullCodecString() { - assertThat(MediaCodecUtil.getCodecProfileAndLevel(/* codec= */ null)).isNull(); + Format format = + Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ 1024, + /* height= */ 768, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null); + assertThat(MediaCodecUtil.getCodecProfileAndLevel(format)).isNull(); } @Test public void getCodecProfileAndLevel_rejectsEmptyCodecString() { - assertThat(MediaCodecUtil.getCodecProfileAndLevel("")).isNull(); + Format format = + Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, + /* codecs= */ "", + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ 1024, + /* height= */ 768, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null); + assertThat(MediaCodecUtil.getCodecProfileAndLevel(format)).isNull(); } private static void assertCodecProfileAndLevelForCodecsString( String codecs, int profile, int level) { - Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codecs); + Format format = + Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_UNKNOWN, + /* codecs= */ codecs, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ 1024, + /* height= */ 768, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null); + assertCodecProfileAndLevelForFormat(format, profile, level); + } + + private static void assertCodecProfileAndLevelForFormat(Format format, int profile, int level) { + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); assertThat(codecProfileAndLevel).isNotNull(); assertThat(codecProfileAndLevel.first).isEqualTo(profile); assertThat(codecProfileAndLevel.second).isEqualTo(level); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java index d870afac3a..88a61d0bce 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.joinByteArrays; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -30,18 +32,19 @@ public final class EventMessageDecoderTest { @Test public void testDecodeEventMessage() { - byte[] rawEmsgBody = new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48000 - 0, 2, 50, -128, // event_duration = 144000 - 0, 15, 67, -45, // id = 1000403 - 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} + byte[] rawEmsgBody = + joinByteArrays( + createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = "urn:test" + createByteArray(49, 50, 51, 0), // value = "123" + createByteArray(0, 0, 11, 184), // event_duration_ms = 3000 + createByteArray(0, 15, 67, 211), // id = 1000403 + createByteArray(0, 1, 2, 3, 4)); // message_data = {0, 1, 2, 3, 4} EventMessageDecoder decoder = new EventMessageDecoder(); MetadataInputBuffer buffer = new MetadataInputBuffer(); buffer.data = ByteBuffer.allocate(rawEmsgBody.length).put(rawEmsgBody); + Metadata metadata = decoder.decode(buffer); + assertThat(metadata.length()).isEqualTo(1); EventMessage eventMessage = (EventMessage) metadata.get(0); assertThat(eventMessage.schemeIdUri).isEqualTo("urn:test"); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java index ca8303d3e2..56830035cc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.android.exoplayer2.testutil.TestUtil.joinByteArrays; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -29,67 +31,52 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class EventMessageEncoderTest { + private static final EventMessage DECODED_MESSAGE = + new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); + + private static final byte[] ENCODED_MESSAGE = + joinByteArrays( + createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = "urn:test" + createByteArray(49, 50, 51, 0), // value = "123" + createByteArray(0, 0, 11, 184), // event_duration_ms = 3000 + createByteArray(0, 15, 67, 211), // id = 1000403 + createByteArray(0, 1, 2, 3, 4)); // message_data = {0, 1, 2, 3, 4} + @Test public void testEncodeEventStream() throws IOException { - EventMessage eventMessage = - new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); - byte[] expectedEmsgBody = - new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, 3, -24, // timescale = 1000 - 0, 0, 0, 0, // presentation_time_delta = 0 - 0, 0, 11, -72, // event_duration = 3000 - 0, 15, 67, -45, // id = 1000403 - 0, 1, 2, 3, 4 - }; // message_data = {0, 1, 2, 3, 4} - byte[] encodedByteArray = new EventMessageEncoder().encode(eventMessage); - assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); + byte[] foo = new byte[] {1, 2, 3}; + + byte[] encodedByteArray = new EventMessageEncoder().encode(DECODED_MESSAGE); + assertThat(encodedByteArray).isEqualTo(ENCODED_MESSAGE); } @Test public void testEncodeDecodeEventStream() throws IOException { - EventMessage expectedEmsg = - new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); - byte[] encodedByteArray = new EventMessageEncoder().encode(expectedEmsg); + byte[] encodedByteArray = new EventMessageEncoder().encode(DECODED_MESSAGE); MetadataInputBuffer buffer = new MetadataInputBuffer(); buffer.data = ByteBuffer.allocate(encodedByteArray.length).put(encodedByteArray); EventMessageDecoder decoder = new EventMessageDecoder(); Metadata metadata = decoder.decode(buffer); assertThat(metadata.length()).isEqualTo(1); - assertThat(metadata.get(0)).isEqualTo(expectedEmsg); + assertThat(metadata.get(0)).isEqualTo(DECODED_MESSAGE); } @Test public void testEncodeEventStreamMultipleTimesWorkingCorrectly() throws IOException { - EventMessage eventMessage = - new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); - byte[] expectedEmsgBody = - new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, 3, -24, // timescale = 1000 - 0, 0, 0, 0, // presentation_time_delta = 0 - 0, 0, 11, -72, // event_duration = 3000 - 0, 15, 67, -45, // id = 1000403 - 0, 1, 2, 3, 4 - }; // message_data = {0, 1, 2, 3, 4} EventMessage eventMessage1 = new EventMessage("urn:test", "123", 3000, 1000402, new byte[] {4, 3, 2, 1, 0}); byte[] expectedEmsgBody1 = - new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, 3, -24, // timescale = 1000 - 0, 0, 0, 0, // presentation_time_delta = 0 - 0, 0, 11, -72, // event_duration = 3000 - 0, 15, 67, -46, // id = 1000402 - 4, 3, 2, 1, 0 - }; // message_data = {4, 3, 2, 1, 0} + joinByteArrays( + createByteArray(117, 114, 110, 58, 116, 101, 115, 116, 0), // scheme_id_uri = "urn:test" + createByteArray(49, 50, 51, 0), // value = "123" + createByteArray(0, 0, 11, 184), // event_duration_ms = 3000 + createByteArray(0, 15, 67, 210), // id = 1000402 + createByteArray(4, 3, 2, 1, 0)); // message_data = {4, 3, 2, 1, 0} + EventMessageEncoder eventMessageEncoder = new EventMessageEncoder(); - byte[] encodedByteArray = eventMessageEncoder.encode(eventMessage); - assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); + byte[] encodedByteArray = eventMessageEncoder.encode(DECODED_MESSAGE); + assertThat(encodedByteArray).isEqualTo(ENCODED_MESSAGE); byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1); assertThat(encodedByteArray1).isEqualTo(expectedEmsgBody1); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java new file mode 100644 index 0000000000..3f07dbc26d --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java @@ -0,0 +1,42 @@ +/* + * 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.metadata.flac; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link PictureFrame}. */ +@RunWith(AndroidJUnit4.class) +public final class PictureFrameTest { + + @Test + public void testParcelable() { + PictureFrame pictureFrameToParcel = new PictureFrame(0, "", "", 0, 0, 0, 0, new byte[0]); + + Parcel parcel = Parcel.obtain(); + pictureFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + PictureFrame pictureFrameFromParcel = PictureFrame.CREATOR.createFromParcel(parcel); + assertThat(pictureFrameFromParcel).isEqualTo(pictureFrameToParcel); + + parcel.recycle(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java new file mode 100644 index 0000000000..bb118e381a --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java @@ -0,0 +1,42 @@ +/* + * 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.metadata.flac; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link VorbisComment}. */ +@RunWith(AndroidJUnit4.class) +public final class VorbisCommentTest { + + @Test + public void testParcelable() { + VorbisComment vorbisCommentFrameToParcel = new VorbisComment("key", "value"); + + Parcel parcel = Parcel.obtain(); + vorbisCommentFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + VorbisComment vorbisCommentFrameFromParcel = VorbisComment.CREATOR.createFromParcel(parcel); + assertThat(vorbisCommentFrameFromParcel).isEqualTo(vorbisCommentFrameToParcel); + + parcel.recycle(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java index 3b78a2e3ae..111edc7af8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.robolectric.shadows.ShadowBaseLooper.shadowMainLooper; import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -36,7 +37,6 @@ import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; @@ -60,9 +60,11 @@ public class DownloadHelperTest { private static final String TEST_DOWNLOAD_TYPE = "downloadType"; private static final String TEST_CACHE_KEY = "cacheKey"; - private static final Timeline TEST_TIMELINE = - new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ new Object())); private static final Object TEST_MANIFEST = new Object(); + private static final Timeline TEST_TIMELINE = + new FakeTimeline( + new Object[] {TEST_MANIFEST}, + new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ new Object())); private static final Format VIDEO_FORMAT_LOW = createVideoFormat(/* bitrate= */ 200_000); private static final Format VIDEO_FORMAT_HIGH = createVideoFormat(/* bitrate= */ 800_000); @@ -111,7 +113,7 @@ public class DownloadHelperTest { testUri, TEST_CACHE_KEY, new TestMediaSource(), - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT, Util.getRendererCapabilities(renderersFactory, /* drmSessionManager= */ null)); } @@ -242,7 +244,7 @@ public class DownloadHelperTest { throws Exception { prepareDownloadHelper(downloadHelper); DefaultTrackSelector.Parameters parameters = - new ParametersBuilder() + new DefaultTrackSelector.ParametersBuilder(ApplicationProvider.getApplicationContext()) .setPreferredAudioLanguage("ZH") .setPreferredTextLanguage("ZH") .setRendererDisabled(/* rendererIndex= */ 2, true) @@ -279,7 +281,7 @@ public class DownloadHelperTest { // Select parameters to require some merging of track groups because the new parameters add // all video tracks to initial video single track selection. DefaultTrackSelector.Parameters parameters = - new ParametersBuilder() + new DefaultTrackSelector.ParametersBuilder(ApplicationProvider.getApplicationContext()) .setPreferredAudioLanguage("ZH") .setPreferredTextLanguage("US") .build(); @@ -383,7 +385,7 @@ public class DownloadHelperTest { // Ensure we have track groups with multiple indices, renderers with multiple track groups and // also renderers without any track groups. DefaultTrackSelector.Parameters parameters = - new ParametersBuilder() + new DefaultTrackSelector.ParametersBuilder(ApplicationProvider.getApplicationContext()) .setPreferredAudioLanguage("ZH") .setPreferredTextLanguage("US") .build(); @@ -491,7 +493,7 @@ public class DownloadHelperTest { private static final class TestMediaSource extends FakeMediaSource { public TestMediaSource() { - super(TEST_TIMELINE, TEST_MANIFEST); + super(TEST_TIMELINE); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 846600f243..89acb3ec3e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -185,6 +185,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline clippedTimeline = getClippedTimeline(timeline, /* durationUs= */ TEST_CLIP_AMOUNT_US); @@ -206,6 +207,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -215,6 +217,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -253,6 +256,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -262,6 +266,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -300,6 +305,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -309,6 +315,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -348,6 +355,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline timeline2 = new SinglePeriodTimeline( @@ -357,6 +365,7 @@ public final class ClippingMediaSourceTest { /* windowDefaultStartPositionUs= */ TEST_CLIP_AMOUNT_US, /* isSeekable= */ true, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); Timeline[] clippedTimelines = @@ -473,7 +482,7 @@ public final class ClippingMediaSourceTest { new SinglePeriodTimeline( TEST_PERIOD_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false); FakeMediaSource fakeMediaSource = - new FakeMediaSource(timeline, /* manifest= */ null) { + new FakeMediaSource(timeline) { @Override protected FakeMediaPeriod createFakeMediaPeriod( MediaPeriodId id, @@ -530,7 +539,7 @@ public final class ClippingMediaSourceTest { */ private static Timeline getClippedTimeline(Timeline timeline, long startUs, long endUs) throws IOException { - FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline); ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startUs, endUs); return getClippedTimelines(fakeMediaSource, mediaSource)[0]; } @@ -540,7 +549,7 @@ public final class ClippingMediaSourceTest { */ private static Timeline getClippedTimeline(Timeline timeline, long durationUs) throws IOException { - FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline); ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, durationUs); return getClippedTimelines(fakeMediaSource, mediaSource)[0]; } @@ -557,7 +566,7 @@ public final class ClippingMediaSourceTest { Timeline firstTimeline, Timeline... additionalTimelines) throws IOException { - FakeMediaSource fakeMediaSource = new FakeMediaSource(firstTimeline, /* manifest= */ null); + FakeMediaSource fakeMediaSource = new FakeMediaSource(firstTimeline); ClippingMediaSource mediaSource = new ClippingMediaSource( fakeMediaSource, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 8137289555..39f36a991b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.MediaSource.SourceInfoRefreshListener; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeMediaSource; @@ -226,7 +226,7 @@ public final class ConcatenatingMediaSourceTest { FakeMediaSource[] fastSources = createMediaSources(2); final FakeMediaSource[] lazySources = new FakeMediaSource[4]; for (int i = 0; i < 4; i++) { - lazySources[i] = new FakeMediaSource(null, null); + lazySources[i] = new FakeMediaSource(null); } // Add lazy sources and normal sources before preparation. Also remove one lazy source again @@ -279,13 +279,6 @@ public final class ConcatenatingMediaSourceTest { CountDownLatch preparedCondition = testRunner.preparePeriod(lazyPeriod, 0); assertThat(preparedCondition.getCount()).isEqualTo(1); - // Assert that a second period can also be created and released without problems. - MediaPeriod secondLazyPeriod = - testRunner.createPeriod( - new MediaPeriodId( - timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); - testRunner.releasePeriod(secondLazyPeriod); - // Trigger source info refresh for lazy media source. Assert that now all information is // available again and the previously created period now also finished preparing. testRunner.runOnPlaybackThread( @@ -314,16 +307,16 @@ public final class ConcatenatingMediaSourceTest { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); - mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); + mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY)); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); mediaSource.addMediaSources( Arrays.asList( new MediaSource[] { - new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), - new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), - new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null) + new FakeMediaSource(Timeline.EMPTY), new FakeMediaSource(Timeline.EMPTY), + new FakeMediaSource(Timeline.EMPTY), new FakeMediaSource(Timeline.EMPTY), + new FakeMediaSource(Timeline.EMPTY), new FakeMediaSource(Timeline.EMPTY) })); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); @@ -369,9 +362,9 @@ public final class ConcatenatingMediaSourceTest { public void testDynamicChangeOfEmptyTimelines() throws IOException { FakeMediaSource[] childSources = new FakeMediaSource[] { - new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), - new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), - new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), + new FakeMediaSource(Timeline.EMPTY), + new FakeMediaSource(Timeline.EMPTY), + new FakeMediaSource(Timeline.EMPTY), }; Timeline nonEmptyTimeline = new FakeTimeline(/* windowCount = */ 1); @@ -394,7 +387,7 @@ public final class ConcatenatingMediaSourceTest { @Test public void testIllegalArguments() { - MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); + MediaSource validSource = new FakeMediaSource(createFakeTimeline(1)); // Null sources. try { @@ -635,15 +628,15 @@ public final class ConcatenatingMediaSourceTest { try { dummyMainThread.runOnMainThread( () -> { - SourceInfoRefreshListener listener = mock(SourceInfoRefreshListener.class); + MediaSourceCaller caller = mock(MediaSourceCaller.class); mediaSource.addMediaSources(Arrays.asList(createMediaSources(2))); - mediaSource.prepareSource(listener, /* mediaTransferListener= */ null); + mediaSource.prepareSource(caller, /* mediaTransferListener= */ null); mediaSource.moveMediaSource( /* currentIndex= */ 0, /* newIndex= */ 1, new Handler(), callbackCalledCondition::open); - mediaSource.releaseSource(listener); + mediaSource.releaseSource(caller); }); assertThat(callbackCalledCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue(); } finally { @@ -667,8 +660,8 @@ public final class ConcatenatingMediaSourceTest { 10 * C.MICROS_PER_SECOND, FakeTimeline.createAdPlaybackState( /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0))); - FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); - FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); + FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly); + FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds); mediaSource.addMediaSource(mediaSourceContentOnly); mediaSource.addMediaSource(mediaSourceWithAds); @@ -814,7 +807,7 @@ public final class ConcatenatingMediaSourceTest { @Test public void testDuplicateMediaSources() throws IOException, InterruptedException { Timeline childTimeline = new FakeTimeline(/* windowCount= */ 2); - FakeMediaSource childSource = new FakeMediaSource(childTimeline, /* manifest= */ null); + FakeMediaSource childSource = new FakeMediaSource(childTimeline); mediaSource.addMediaSource(childSource); mediaSource.addMediaSource(childSource); @@ -847,7 +840,7 @@ public final class ConcatenatingMediaSourceTest { @Test public void testDuplicateNestedMediaSources() throws IOException, InterruptedException { Timeline childTimeline = new FakeTimeline(/* windowCount= */ 1); - FakeMediaSource childSource = new FakeMediaSource(childTimeline, /* manifest= */ null); + FakeMediaSource childSource = new FakeMediaSource(childTimeline); ConcatenatingMediaSource nestedConcatenation = new ConcatenatingMediaSource(); testRunner.prepareSource(); @@ -881,8 +874,7 @@ public final class ConcatenatingMediaSourceTest { public void testClear() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); final FakeMediaSource preparedChildSource = createFakeMediaSource(); - final FakeMediaSource unpreparedChildSource = - new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + final FakeMediaSource unpreparedChildSource = new FakeMediaSource(/* timeline= */ null); dummyMainThread.runOnMainThread( () -> { mediaSource.addMediaSource(preparedChildSource); @@ -1099,13 +1091,13 @@ public final class ConcatenatingMediaSourceTest { private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { - sources[i] = new FakeMediaSource(createFakeTimeline(i), null); + sources[i] = new FakeMediaSource(createFakeTimeline(i)); } return sources; } private static FakeMediaSource createFakeMediaSource() { - return new FakeMediaSource(createFakeTimeline(/* index */ 0), null); + return new FakeMediaSource(createFakeTimeline(/* index */ 0)); } private static FakeTimeline createFakeTimeline(int index) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index df6506ed52..fa7c2f0614 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -135,7 +135,7 @@ public class LoopingMediaSourceTest { * Wraps the specified timeline in a {@link LoopingMediaSource} and returns the looping timeline. */ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) throws IOException { - FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline); LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); try { @@ -153,7 +153,7 @@ public class LoopingMediaSourceTest { * the looping timeline can be created and prepared. */ private static void testMediaPeriodCreation(Timeline timeline, int loopCount) throws Exception { - FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline); LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); try { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java index 5ea15ac2e8..1434d28500 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java @@ -68,8 +68,7 @@ public class MergingMediaSourceTest { public void testMergingMediaSourcePeriodCreation() throws Exception { FakeMediaSource[] mediaSources = new FakeMediaSource[2]; for (int i = 0; i < mediaSources.length; i++) { - mediaSources[i] = - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null); + mediaSources[i] = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2)); } MergingMediaSource mediaSource = new MergingMediaSource(mediaSources); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); @@ -92,7 +91,7 @@ public class MergingMediaSourceTest { private static void testMergingMediaSourcePrepare(Timeline... timelines) throws IOException { FakeMediaSource[] mediaSources = new FakeMediaSource[timelines.length]; for (int i = 0; i < timelines.length; i++) { - mediaSources[i] = new FakeMediaSource(timelines[i], null); + mediaSources[i] = new FakeMediaSource(timelines[i]); } MergingMediaSource mergingMediaSource = new MergingMediaSource(mediaSources); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mergingMediaSource, null); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 450f0ecd3a..6812e08ef7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -29,10 +29,12 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Arrays; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -89,6 +91,8 @@ public final class SampleQueueTest { private static final Format[] SAMPLE_FORMATS = new Format[] {FORMAT_1, FORMAT_1, FORMAT_1, FORMAT_1, FORMAT_2, FORMAT_2, FORMAT_2, FORMAT_2}; private static final int DATA_SECOND_KEYFRAME_INDEX = 4; + private static final TrackOutput.CryptoData DUMMY_CRYPTO_DATA = + new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0); private Allocator allocator; private SampleQueue sampleQueue; @@ -125,10 +129,10 @@ public final class SampleQueueTest { } @Test - public void testReadFormatDeduplicated() { + public void testEqualFormatsDeduplicated() { sampleQueue.format(FORMAT_1); assertReadFormat(false, FORMAT_1); - // If the same format is input then it should be de-duplicated (i.e. not output again). + // If the same format is written then it should not cause a format change on the read side. sampleQueue.format(FORMAT_1); assertNoSamplesToRead(FORMAT_1); // The same applies for a format that's equal (but a different object). @@ -136,6 +140,33 @@ public final class SampleQueueTest { assertNoSamplesToRead(FORMAT_1); } + @Test + public void testMultipleFormatsDeduplicated() { + sampleQueue.format(FORMAT_1); + sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); + sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); + // Writing multiple formats should not cause a format change on the read side, provided the last + // format to be written is equal to the format of the previous sample. + sampleQueue.format(FORMAT_2); + sampleQueue.format(FORMAT_1_COPY); + sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); + sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); + + assertReadFormat(false, FORMAT_1); + assertReadSample(0, true, DATA, 0, ALLOCATION_SIZE); + // Assert the second sample is read without a format change. + assertReadSample(1000, true, DATA, 0, ALLOCATION_SIZE); + + // The same applies if the queue is empty when the formats are written. + sampleQueue.format(FORMAT_2); + sampleQueue.format(FORMAT_1); + sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); + sampleQueue.sampleMetadata(2000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); + + // Assert the third sample is read without a format change. + assertReadSample(2000, true, DATA, 0, ALLOCATION_SIZE); + } + @Test public void testReadSingleSamples() { sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); @@ -511,6 +542,49 @@ public final class SampleQueueTest { assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE); } + @Test + public void testAllowOnlyClearBuffers() { + int[] flags = + new int[] { + C.BUFFER_FLAG_KEY_FRAME, + C.BUFFER_FLAG_ENCRYPTED, + 0, + 0, + 0, + C.BUFFER_FLAG_KEY_FRAME | C.BUFFER_FLAG_ENCRYPTED, + 0, + 0 + }; + int[] sampleSizes = new int[flags.length]; + Arrays.fill(sampleSizes, /* val= */ 1); + + // Two encryption preamble bytes per encrypted sample in the sample queue. + byte[] sampleData = new byte[flags.length + 2 + 2]; + Arrays.fill(sampleData, /* val= */ (byte) 1); + + writeTestData( + sampleData, sampleSizes, new int[flags.length], SAMPLE_TIMESTAMPS, SAMPLE_FORMATS, flags); + assertReadFormat(/* formatRequired= */ false, FORMAT_1); + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ true); + + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ false); + + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_FORMAT_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ true); + + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ false); + + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); + assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ true); + + assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ false); + } + @Test public void testLargestQueuedTimestampWithRead() { writeTestData(); @@ -602,8 +676,12 @@ public final class SampleQueueTest { sampleQueue.format(sampleFormats[i]); format = sampleFormats[i]; } - sampleQueue.sampleMetadata(sampleTimestamps[i], sampleFlags[i], sampleSizes[i], - sampleOffsets[i], null); + sampleQueue.sampleMetadata( + sampleTimestamps[i], + sampleFlags[i], + sampleSizes[i], + sampleOffsets[i], + (sampleFlags[i] & C.BUFFER_FLAG_ENCRYPTED) != 0 ? DUMMY_CRYPTO_DATA : null); } } @@ -714,11 +792,18 @@ public final class SampleQueueTest { /** * Asserts {@link SampleQueue#read} returns {@link C#RESULT_NOTHING_READ}. * - * @param formatRequired The value of {@code formatRequired} passed to readData. + * @param formatRequired The value of {@code formatRequired} passed to {@link SampleQueue#read}. */ private void assertReadNothing(boolean formatRequired) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0); + int result = + sampleQueue.read( + formatHolder, + inputBuffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_NOTHING_READ); // formatHolder should not be populated. assertThat(formatHolder.format).isNull(); @@ -728,14 +813,21 @@ public final class SampleQueueTest { } /** - * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the - * {@link DecoderInputBuffer#isEndOfStream()} is set. + * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the {@link + * DecoderInputBuffer#isEndOfStream()} is set. * - * @param formatRequired The value of {@code formatRequired} passed to readData. + * @param formatRequired The value of {@code formatRequired} passed to {@link SampleQueue#read}. */ private void assertReadEndOfStream(boolean formatRequired) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, true, 0); + int result = + sampleQueue.read( + formatHolder, + inputBuffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + /* loadingFinished= */ true, + /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_BUFFER_READ); // formatHolder should not be populated. assertThat(formatHolder.format).isNull(); @@ -750,12 +842,19 @@ public final class SampleQueueTest { * Asserts {@link SampleQueue#read} returns {@link C#RESULT_FORMAT_READ} and that the format * holder is filled with a {@link Format} that equals {@code format}. * - * @param formatRequired The value of {@code formatRequired} passed to readData. + * @param formatRequired The value of {@code formatRequired} passed to {@link SampleQueue#read}. * @param format The expected format. */ private void assertReadFormat(boolean formatRequired, Format format) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0); + int result = + sampleQueue.read( + formatHolder, + inputBuffer, + formatRequired, + /* allowOnlyClearBuffers= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_FORMAT_READ); // formatHolder should be populated. assertThat(formatHolder.format).isEqualTo(format); @@ -777,7 +876,14 @@ public final class SampleQueueTest { private void assertReadSample( long timeUs, boolean isKeyframe, byte[] sampleData, int offset, int length) { clearFormatHolderAndInputBuffer(); - int result = sampleQueue.read(formatHolder, inputBuffer, false, false, 0); + int result = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + /* allowOnlyClearBuffers= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_BUFFER_READ); // formatHolder should not be populated. assertThat(formatHolder.format).isNull(); @@ -793,6 +899,19 @@ public final class SampleQueueTest { assertThat(readData).isEqualTo(copyOfRange(sampleData, offset, offset + length)); } + /** Asserts {@link SampleQueue#read} returns the given result. */ + private void assertResult(int expectedResult, boolean allowOnlyClearBuffers) { + int obtainedResult = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + allowOnlyClearBuffers, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); + assertThat(obtainedResult).isEqualTo(expectedResult); + } + /** * Asserts the number of allocations currently in use by {@code sampleQueue}. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java index 8fce472c68..17b0996387 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java @@ -123,7 +123,8 @@ public final class ShuffleOrderTest { assertThat(shuffleOrder.getLastIndex()).isEqualTo(indices[length - 1]); assertThat(shuffleOrder.getNextIndex(indices[length - 1])).isEqualTo(INDEX_UNSET); for (int i = 0; i < length; i++) { - assertThat(indices[i] >= 0 && indices[i] < length).isTrue(); + assertThat(indices[i] >= 0).isTrue(); + assertThat(indices[i] < length).isTrue(); } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java index bdd6820efa..701ec3521c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java @@ -62,6 +62,7 @@ public final class SinglePeriodTimelineTest { /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ false, /* isDynamic= */ true, + /* manifest= */ null, /* tag= */ null); // Should return null with a positive position projection beyond window duration. Pair position = @@ -84,6 +85,7 @@ public final class SinglePeriodTimelineTest { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* manifest= */ null, /* tag= */ null); assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ false).tag).isNull(); @@ -100,7 +102,11 @@ public final class SinglePeriodTimelineTest { Object tag = new Object(); SinglePeriodTimeline timeline = new SinglePeriodTimeline( - /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, tag); + /* durationUs= */ C.TIME_UNSET, + /* isSeekable= */ false, + /* isDynamic= */ false, + /* manifest= */ null, + tag); assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ false).tag).isNull(); assertThat(timeline.getWindow(/* windowIndex= */ 0, window, /* setTag= */ true).tag) @@ -114,6 +120,7 @@ public final class SinglePeriodTimelineTest { /* durationUs= */ C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ false, + /* manifest= */ null, /* tag= */ null); Object uid = timeline.getPeriod(/* periodIndex= */ 0, period, /* setIds= */ true).uid; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index ab67ac115b..7095962801 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.text.Subtitle; import java.io.IOException; import java.util.ArrayList; import org.junit.Test; @@ -41,7 +42,7 @@ public final class SsaDecoderTest { public void testDecodeEmpty() throws IOException { SsaDecoder decoder = new SsaDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY); - SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(0); assertThat(subtitle.getCues(0).isEmpty()).isTrue(); @@ -51,7 +52,7 @@ public final class SsaDecoderTest { public void testDecodeTypical() throws IOException { SsaDecoder decoder = new SsaDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL); - SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); assertTypicalCue1(subtitle, 0); @@ -71,7 +72,7 @@ public final class SsaDecoderTest { SsaDecoder decoder = new SsaDecoder(initializationData); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_DIALOGUE_ONLY); - SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); assertTypicalCue1(subtitle, 0); @@ -85,7 +86,7 @@ public final class SsaDecoderTest { SsaDecoder decoder = new SsaDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INVALID_TIMECODES); - SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(2); assertTypicalCue3(subtitle, 0); @@ -96,7 +97,7 @@ public final class SsaDecoderTest { SsaDecoder decoder = new SsaDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_END_TIMECODES); - SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(3); @@ -113,21 +114,21 @@ public final class SsaDecoderTest { .isEqualTo("This is the third subtitle, with a comma."); } - private static void assertTypicalCue1(SsaSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the first subtitle."); assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(1230000); } - private static void assertTypicalCue2(SsaSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue2(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(2340000); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the second subtitle \nwith a newline \nand another."); assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(3450000); } - private static void assertTypicalCue3(SsaSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue3(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(4560000); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the third subtitle, with a comma."); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java index 9520262207..774e8d98b9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java @@ -21,6 +21,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,7 +45,7 @@ public final class SubripDecoderTest { public void testDecodeEmpty() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), EMPTY_FILE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(0); assertThat(subtitle.getCues(0).isEmpty()).isTrue(); @@ -54,7 +55,7 @@ public final class SubripDecoderTest { public void testDecodeTypical() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_FILE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); assertTypicalCue1(subtitle, 0); @@ -68,7 +69,7 @@ public final class SubripDecoderTest { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_WITH_BYTE_ORDER_MARK); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); assertTypicalCue1(subtitle, 0); @@ -82,7 +83,7 @@ public final class SubripDecoderTest { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_EXTRA_BLANK_LINE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); assertTypicalCue1(subtitle, 0); @@ -97,7 +98,7 @@ public final class SubripDecoderTest { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_TIMECODE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); assertTypicalCue1(subtitle, 0); @@ -111,7 +112,7 @@ public final class SubripDecoderTest { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_MISSING_SEQUENCE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); assertTypicalCue1(subtitle, 0); @@ -125,7 +126,7 @@ public final class SubripDecoderTest { byte[] bytes = TestUtil.getByteArray( ApplicationProvider.getApplicationContext(), TYPICAL_NEGATIVE_TIMESTAMPS); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(2); assertTypicalCue3(subtitle, 0); @@ -137,7 +138,7 @@ public final class SubripDecoderTest { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UNEXPECTED_END); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); assertTypicalCue1(subtitle, 0); @@ -149,7 +150,7 @@ public final class SubripDecoderTest { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_END_TIMECODES_FILE); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(3); @@ -171,7 +172,7 @@ public final class SubripDecoderTest { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS); - SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -194,21 +195,21 @@ public final class SubripDecoderTest { assertAlignmentCue(subtitle, 26, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_END); // {/an9} } - private static void assertTypicalCue1(SubripSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the first subtitle."); assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(1234000); } - private static void assertTypicalCue2(SubripSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue2(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(2345000); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the second subtitle.\nSecond subtitle with second line."); assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(3456000); } - private static void assertTypicalCue3(SubripSubtitle subtitle, int eventIndex) { + private static void assertTypicalCue3(Subtitle subtitle, int eventIndex) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(4567000); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the third subtitle."); @@ -216,7 +217,7 @@ public final class SubripDecoderTest { } private static void assertAlignmentCue( - SubripSubtitle subtitle, + Subtitle subtitle, int eventIndex, @Cue.AnchorType int lineAnchor, @Cue.AnchorType int positionAnchor) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index 000d0634ce..22c7288340 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -514,7 +514,7 @@ public final class TtmlDecoderTest { assertThat(cue.position).isEqualTo(24f / 100f); assertThat(cue.line).isEqualTo(28f / 100f); assertThat(cue.size).isEqualTo(51f / 100f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(12f / 100f); cues = subtitle.getCues(4000000); assertThat(cues).hasSize(1); @@ -524,7 +524,7 @@ public final class TtmlDecoderTest { assertThat(cue.position).isEqualTo(21f / 100f); assertThat(cue.line).isEqualTo(35f / 100f); assertThat(cue.size).isEqualTo(57f / 100f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(6f / 100f); cues = subtitle.getCues(7500000); assertThat(cues).hasSize(1); @@ -534,7 +534,7 @@ public final class TtmlDecoderTest { assertThat(cue.position).isEqualTo(24f / 100f); assertThat(cue.line).isEqualTo(28f / 100f); assertThat(cue.size).isEqualTo(51f / 100f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(12f / 100f); } @Test @@ -549,7 +549,7 @@ public final class TtmlDecoderTest { assertThat(cue.position).isEqualTo(307f / 1280f); assertThat(cue.line).isEqualTo(562f / 720f); assertThat(cue.size).isEqualTo(653f / 1280f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(86f / 720f); cues = subtitle.getCues(4000000); assertThat(cues).hasSize(1); @@ -559,7 +559,7 @@ public final class TtmlDecoderTest { assertThat(cue.position).isEqualTo(269f / 1280f); assertThat(cue.line).isEqualTo(612f / 720f); assertThat(cue.size).isEqualTo(730f / 1280f); - assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET); + assertThat(cue.bitmapHeight).isEqualTo(43f / 720f); } @Test @@ -701,6 +701,6 @@ public final class TtmlDecoderTest { private TtmlSubtitle getSubtitle(String file) throws IOException, SubtitleDecoderException { TtmlDecoder ttmlDecoder = new TtmlDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file); - return ttmlDecoder.decode(bytes, bytes.length, false); + return (TtmlSubtitle) ttmlDecoder.decode(bytes, bytes.length, false); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java index fb9e1a875f..72be083181 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -87,21 +88,32 @@ public final class CssParserTest { @Test public void testParseMethodSimpleInput() { - String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }"; WebvttCssStyle expectedStyle = new WebvttCssStyle(); + String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }"; expectedStyle.setFontColor(0xFF000000); expectedStyle.setBackgroundColor(0xFFFFEFD5); - assertParserProduces(expectedStyle, styleBlock1); + assertParserProduces(styleBlock1, expectedStyle); String styleBlock2 = " ::cue { color : black }\n\n::cue { color : invalid }"; expectedStyle = new WebvttCssStyle(); expectedStyle.setFontColor(0xFF000000); - assertParserProduces(expectedStyle, styleBlock2); + assertParserProduces(styleBlock2, expectedStyle); - String styleBlock3 = " \n::cue {\n background-color\n:#00fFFe}"; + String styleBlock3 = "::cue {\n background-color\n:#00fFFe}"; expectedStyle = new WebvttCssStyle(); expectedStyle.setBackgroundColor(0xFF00FFFE); - assertParserProduces(expectedStyle, styleBlock3); + assertParserProduces(styleBlock3, expectedStyle); + } + + @Test + public void testParseMethodMultipleRulesInBlockInput() { + String styleBlock = + "::cue {\n background-color\n:#00fFFe} \n::cue {\n background-color\n:#00000000}\n"; + WebvttCssStyle expectedStyle = new WebvttCssStyle(); + expectedStyle.setBackgroundColor(0xFF00FFFE); + WebvttCssStyle secondExpectedStyle = new WebvttCssStyle(); + secondExpectedStyle.setBackgroundColor(0x000000); + assertParserProduces(styleBlock, expectedStyle, secondExpectedStyle); } @Test @@ -116,7 +128,7 @@ public final class CssParserTest { expectedStyle.setFontFamily("courier"); expectedStyle.setBold(true); - assertParserProduces(expectedStyle, styleBlock); + assertParserProduces(styleBlock, expectedStyle); } @Test @@ -128,7 +140,7 @@ public final class CssParserTest { expectedStyle.setBackgroundColor(0x190A0B0C); expectedStyle.setFontColor(0xFF010101); - assertParserProduces(expectedStyle, styleBlock); + assertParserProduces(styleBlock, expectedStyle); } @Test @@ -203,25 +215,29 @@ public final class CssParserTest { assertThat(input.readLine()).isEqualTo(expectedLine); } - private void assertParserProduces(WebvttCssStyle expected, - String styleBlock){ + private void assertParserProduces(String styleBlock, WebvttCssStyle... expectedStyles) { ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(styleBlock)); - WebvttCssStyle actualElem = parser.parseBlock(input); - assertThat(actualElem.hasBackgroundColor()).isEqualTo(expected.hasBackgroundColor()); - if (expected.hasBackgroundColor()) { - assertThat(actualElem.getBackgroundColor()).isEqualTo(expected.getBackgroundColor()); + List styles = parser.parseBlock(input); + assertThat(styles.size()).isEqualTo(expectedStyles.length); + for (int i = 0; i < expectedStyles.length; i++) { + WebvttCssStyle expected = expectedStyles[i]; + WebvttCssStyle actualElem = styles.get(i); + assertThat(actualElem.hasBackgroundColor()).isEqualTo(expected.hasBackgroundColor()); + if (expected.hasBackgroundColor()) { + assertThat(actualElem.getBackgroundColor()).isEqualTo(expected.getBackgroundColor()); + } + assertThat(actualElem.hasFontColor()).isEqualTo(expected.hasFontColor()); + if (expected.hasFontColor()) { + assertThat(actualElem.getFontColor()).isEqualTo(expected.getFontColor()); + } + assertThat(actualElem.getFontFamily()).isEqualTo(expected.getFontFamily()); + assertThat(actualElem.getFontSize()).isEqualTo(expected.getFontSize()); + assertThat(actualElem.getFontSizeUnit()).isEqualTo(expected.getFontSizeUnit()); + assertThat(actualElem.getStyle()).isEqualTo(expected.getStyle()); + assertThat(actualElem.isLinethrough()).isEqualTo(expected.isLinethrough()); + assertThat(actualElem.isUnderline()).isEqualTo(expected.isUnderline()); + assertThat(actualElem.getTextAlign()).isEqualTo(expected.getTextAlign()); } - assertThat(actualElem.hasFontColor()).isEqualTo(expected.hasFontColor()); - if (expected.hasFontColor()) { - assertThat(actualElem.getFontColor()).isEqualTo(expected.getFontColor()); - } - assertThat(actualElem.getFontFamily()).isEqualTo(expected.getFontFamily()); - assertThat(actualElem.getFontSize()).isEqualTo(expected.getFontSize()); - assertThat(actualElem.getFontSizeUnit()).isEqualTo(expected.getFontSizeUnit()); - assertThat(actualElem.getStyle()).isEqualTo(expected.getStyle()); - assertThat(actualElem.isLinethrough()).isEqualTo(expected.isLinethrough()); - assertThat(actualElem.isUnderline()).isEqualTo(expected.isUnderline()); - assertThat(actualElem.getTextAlign()).isEqualTo(expected.getTextAlign()); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 2a7289c039..9320a3f31c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -395,7 +395,7 @@ public class WebvttDecoderTest { throws IOException, SubtitleDecoderException { WebvttDecoder decoder = new WebvttDecoder(); byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), asset); - return decoder.decode(bytes, bytes.length, /* reset= */ false); + return (WebvttSubtitle) decoder.decode(bytes, bytes.length, /* reset= */ false); } private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java index b077a92d99..456f7f7107 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.trackselection; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; @@ -392,6 +392,7 @@ public final class AdaptiveTrackSelectionTest { trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, + /* reservedBandwidth= */ 0, minDurationForQualityIncreaseMs, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -408,6 +409,7 @@ public final class AdaptiveTrackSelectionTest { trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, + /* reservedBandwidth= */ 0, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, maxDurationForQualityDecreaseMs, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -426,6 +428,7 @@ public final class AdaptiveTrackSelectionTest { trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, + /* reservedBandwidth= */ 0, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, durationToRetainAfterDiscardMs, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index e5dd4ae636..0374f88bae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -27,9 +27,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; +import android.content.Context; import android.os.Parcel; import android.util.SparseArray; import android.util.SparseBooleanArray; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -97,6 +99,7 @@ public final class DefaultTrackSelectorTest { @Mock private InvalidationListener invalidationListener; @Mock private BandwidthMeter bandwidthMeter; + private Parameters defaultParameters; private DefaultTrackSelector trackSelector; @BeforeClass @@ -108,7 +111,9 @@ public final class DefaultTrackSelectorTest { public void setUp() { initMocks(this); when(bandwidthMeter.getBitrateEstimate()).thenReturn(1000000L); - trackSelector = new DefaultTrackSelector(); + Context context = ApplicationProvider.getApplicationContext(); + defaultParameters = Parameters.getDefaults(context); + trackSelector = new DefaultTrackSelector(context); trackSelector.init(invalidationListener, bandwidthMeter); } @@ -143,6 +148,7 @@ public final class DefaultTrackSelectorTest { /* exceedAudioConstraintsIfNecessary= */ false, /* allowAudioMixedMimeTypeAdaptiveness= */ true, /* allowAudioMixedSampleRateAdaptiveness= */ false, + /* allowAudioMixedChannelCountAdaptiveness= */ true, // Text /* preferredTextLanguage= */ "de", /* selectUndeterminedTextLanguage= */ true, @@ -234,7 +240,7 @@ public final class DefaultTrackSelectorTest { /** Tests disabling a renderer. */ @Test public void testSelectTracksWithDisabledRenderer() throws ExoPlaybackException { - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setRendererDisabled(1, true)); + trackSelector.setParameters(defaultParameters.buildUpon().setRendererDisabled(1, true)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS, periodId, TIMELINE); assertSelections(result, new TrackSelection[] {TRACK_SELECTIONS[0], null}); @@ -271,7 +277,7 @@ public final class DefaultTrackSelectorTest { /** Tests disabling a no-sample renderer. */ @Test public void testSelectTracksWithDisabledNoSampleRenderer() throws ExoPlaybackException { - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setRendererDisabled(1, true)); + trackSelector.setParameters(defaultParameters.buildUpon().setRendererDisabled(1, true)); TrackSelectorResult result = trackSelector.selectTracks( RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS, periodId, TIMELINE); @@ -281,14 +287,13 @@ public final class DefaultTrackSelectorTest { } /** - * Tests that track selector will not call - * {@link InvalidationListener#onTrackSelectionsInvalidated()} when it's set with default - * values of {@link Parameters}. + * Tests that track selector will not call {@link + * InvalidationListener#onTrackSelectionsInvalidated()} when it's set with default values of + * {@link Parameters}. */ @Test - public void testSetParameterWithDefaultParametersDoesNotNotifyInvalidationListener() - throws Exception { - trackSelector.setParameters(Parameters.DEFAULT); + public void testSetParameterWithDefaultParametersDoesNotNotifyInvalidationListener() { + trackSelector.setParameters(defaultParameters); verify(invalidationListener, never()).onTrackSelectionsInvalidated(); } @@ -297,24 +302,22 @@ public final class DefaultTrackSelectorTest { * when it's set with non-default values of {@link Parameters}. */ @Test - public void testSetParameterWithNonDefaultParameterNotifyInvalidationListener() - throws Exception { - Parameters parameters = Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng").build(); - trackSelector.setParameters(parameters); + public void testSetParameterWithNonDefaultParameterNotifyInvalidationListener() { + ParametersBuilder builder = defaultParameters.buildUpon().setPreferredAudioLanguage("eng"); + trackSelector.setParameters(builder); verify(invalidationListener).onTrackSelectionsInvalidated(); } /** - * Tests that track selector will not call - * {@link InvalidationListener#onTrackSelectionsInvalidated()} again when it's set with - * the same values of {@link Parameters}. + * Tests that track selector will not call {@link + * InvalidationListener#onTrackSelectionsInvalidated()} again when it's set with the same values + * of {@link Parameters}. */ @Test - public void testSetParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain() - throws Exception { - ParametersBuilder builder = Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng"); - trackSelector.setParameters(builder.build()); - trackSelector.setParameters(builder.build()); + public void testSetParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain() { + ParametersBuilder builder = defaultParameters.buildUpon().setPreferredAudioLanguage("eng"); + trackSelector.setParameters(builder); + trackSelector.setParameters(builder); verify(invalidationListener, times(1)).onTrackSelectionsInvalidated(); } @@ -341,6 +344,76 @@ public final class DefaultTrackSelectorTest { assertFixedSelection(result.selections.get(0), trackGroups, formatWithSelectionFlag); } + /** Tests that adaptive audio track selections respect the maximum audio bitrate. */ + public void testSelectAdaptiveAudioTrackGroupWithMaxBitrate() throws ExoPlaybackException { + Format format128k = + Format.createAudioSampleFormat( + /* id= */ "128", + /* sampleMimeType= */ MimeTypes.AUDIO_AAC, + /* codecs= */ "mp4a.40.2", + /* bitrate= */ 128 * 1024, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + Format format192k = + Format.createAudioSampleFormat( + /* id= */ "192", + /* sampleMimeType= */ MimeTypes.AUDIO_AAC, + /* codecs= */ "mp4a.40.2", + /* bitrate= */ 192 * 1024, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + Format format256k = + Format.createAudioSampleFormat( + /* id= */ "256", + /* sampleMimeType= */ MimeTypes.AUDIO_AAC, + /* codecs= */ "mp4a.40.2", + /* bitrate= */ 256 * 1024, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + RendererCapabilities[] rendererCapabilities = { + ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES + }; + TrackGroupArray trackGroups = + new TrackGroupArray(new TrackGroup(format192k, format128k, format256k)); + + TrackSelectorResult result = + trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); + assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1, 2); + + trackSelector.setParameters( + trackSelector.buildUponParameters().setMaxAudioBitrate(256 * 1024 - 1)); + result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); + assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1); + + trackSelector.setParameters(trackSelector.buildUponParameters().setMaxAudioBitrate(192 * 1024)); + result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); + assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1); + + trackSelector.setParameters( + trackSelector.buildUponParameters().setMaxAudioBitrate(192 * 1024 - 1)); + result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); + assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1); + + trackSelector.setParameters(trackSelector.buildUponParameters().setMaxAudioBitrate(10)); + result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); + assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1); + } + /** * Tests that track selector will select audio track with language that match preferred language * given by {@link Parameters}. @@ -356,8 +429,7 @@ public final class DefaultTrackSelectorTest { Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); TrackGroupArray trackGroups = wrapFormats(frAudioFormat, enAudioFormat); - trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng").build()); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("eng")); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, @@ -382,8 +454,7 @@ public final class DefaultTrackSelectorTest { Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); TrackGroupArray trackGroups = wrapFormats(frAudioFormat, enAudioFormat); - trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng").build()); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("eng")); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, @@ -453,7 +524,7 @@ public final class DefaultTrackSelectorTest { TrackGroupArray trackGroups = singleTrackGroup(audioFormat); trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setExceedRendererCapabilitiesIfNecessary(false).build()); + defaultParameters.buildUpon().setExceedRendererCapabilitiesIfNecessary(false)); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES}, @@ -535,8 +606,7 @@ public final class DefaultTrackSelectorTest { RendererCapabilities mappedAudioRendererCapabilities = new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); - trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng").build()); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("eng")); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {mappedAudioRendererCapabilities}, @@ -578,8 +648,7 @@ public final class DefaultTrackSelectorTest { RendererCapabilities mappedAudioRendererCapabilities = new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); - trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("eng").build()); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("eng")); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {mappedAudioRendererCapabilities}, @@ -893,7 +962,6 @@ public final class DefaultTrackSelectorTest { Format forcedDefault = buildTextFormat("forcedDefault", "eng", C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT); Format defaultOnly = buildTextFormat("defaultOnly", "eng", C.SELECTION_FLAG_DEFAULT); - Format forcedOnlySpanish = buildTextFormat("forcedOnlySpanish", "spa", C.SELECTION_FLAG_FORCED); Format noFlag = buildTextFormat("noFlag", "eng"); RendererCapabilities[] textRendererCapabilities = @@ -914,10 +982,7 @@ public final class DefaultTrackSelectorTest { // selected. trackGroups = wrapFormats(defaultOnly, noFlag, forcedOnly, forcedDefault); trackSelector.setParameters( - Parameters.DEFAULT - .buildUpon() - .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT) - .build()); + defaultParameters.buildUpon().setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT)); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(0)); @@ -928,17 +993,16 @@ public final class DefaultTrackSelectorTest { trackSelector .getParameters() .buildUpon() - .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED) - .build()); + .setDisabledTextTrackSelectionFlags( + C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED)); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(0)); - // There is a preferred language, so the first language-matching track flagged as default should - // be selected. - trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("eng").build()); + // There is a preferred language, so a language-matching track flagged as default should + // be selected, and the one without forced flag should be preferred. + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredTextLanguage("eng")); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); - assertFixedSelection(result.selections.get(0), trackGroups, forcedDefault); + assertFixedSelection(result.selections.get(0), trackGroups, defaultOnly); // Same as above, but the default flag is disabled. If multiple tracks match the preferred // language, those not flagged as forced are preferred, as they likely include the contents of @@ -948,8 +1012,7 @@ public final class DefaultTrackSelectorTest { trackSelector .getParameters() .buildUpon() - .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT) - .build()); + .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT)); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, noFlag); } @@ -1031,12 +1094,12 @@ public final class DefaultTrackSelectorTest { assertNoSelection(result.selections.get(0)); trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setSelectUndeterminedTextLanguage(true).build()); + defaultParameters.buildUpon().setSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, undeterminedUnd); - ParametersBuilder builder = Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("spa"); - trackSelector.setParameters(builder.build()); + ParametersBuilder builder = defaultParameters.buildUpon().setPreferredTextLanguage("spa"); + trackSelector.setParameters(builder); result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, spanish); @@ -1045,7 +1108,7 @@ public final class DefaultTrackSelectorTest { result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(0)); - trackSelector.setParameters(builder.setSelectUndeterminedTextLanguage(true).build()); + trackSelector.setParameters(builder.setSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, undeterminedUnd); @@ -1089,13 +1152,13 @@ public final class DefaultTrackSelectorTest { assertNoSelection(result.selections.get(1)); // Explicit language preference for english. First renderer should be used. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("en")); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredTextLanguage("en")); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, english); assertNoSelection(result.selections.get(1)); // Explicit language preference for German. Second renderer should be used. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("de")); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredTextLanguage("de")); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(0)); assertFixedSelection(result.selections.get(1), trackGroups, german); @@ -1121,7 +1184,7 @@ public final class DefaultTrackSelectorTest { RendererCapabilities mappedAudioRendererCapabilities = new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setForceLowestBitrate(true).build()); + trackSelector.setParameters(defaultParameters.buildUpon().setForceLowestBitrate(true)); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {mappedAudioRendererCapabilities}, @@ -1152,7 +1215,7 @@ public final class DefaultTrackSelectorTest { new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); trackSelector.setParameters( - new ParametersBuilder().setForceHighestSupportedBitrate(true).build()); + defaultParameters.buildUpon().setForceHighestSupportedBitrate(true)); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {mappedAudioRendererCapabilities}, @@ -1200,7 +1263,7 @@ public final class DefaultTrackSelectorTest { // If we explicitly enable mixed sample rate adaptiveness, expect an adaptive selection. trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setAllowAudioMixedSampleRateAdaptiveness(true)); + defaultParameters.buildUpon().setAllowAudioMixedSampleRateAdaptiveness(true)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1232,7 +1295,7 @@ public final class DefaultTrackSelectorTest { // If we explicitly enable mixed mime type adaptiveness, expect an adaptive selection. trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setAllowAudioMixedMimeTypeAdaptiveness(true)); + defaultParameters.buildUpon().setAllowAudioMixedMimeTypeAdaptiveness(true)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1266,7 +1329,7 @@ public final class DefaultTrackSelectorTest { // If we constrain the channel count to 4 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setMaxAudioChannelCount(4)); + trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(4)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1275,7 +1338,7 @@ public final class DefaultTrackSelectorTest { // If we constrain the channel count to 2 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setMaxAudioChannelCount(2)); + trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(2)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1284,7 +1347,7 @@ public final class DefaultTrackSelectorTest { // If we constrain the channel count to 1 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setMaxAudioChannelCount(1)); + trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(1)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1293,7 +1356,7 @@ public final class DefaultTrackSelectorTest { // If we disable exceeding of constraints we expect no selection. trackSelector.setParameters( - Parameters.DEFAULT + defaultParameters .buildUpon() .setMaxAudioChannelCount(1) .setExceedAudioConstraintsIfNecessary(false)); @@ -1355,13 +1418,13 @@ public final class DefaultTrackSelectorTest { assertNoSelection(result.selections.get(1)); // Explicit language preference for english. First renderer should be used. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("en")); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("en")); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); assertFixedSelection(result.selections.get(0), trackGroups, english); assertNoSelection(result.selections.get(1)); // Explicit language preference for German. Second renderer should be used. - trackSelector.setParameters(Parameters.DEFAULT.buildUpon().setPreferredAudioLanguage("de")); + trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("de")); result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); assertNoSelection(result.selections.get(0)); assertFixedSelection(result.selections.get(1), trackGroups, german); @@ -1387,7 +1450,7 @@ public final class DefaultTrackSelectorTest { // Should do non-seamless adaptiveness by default, so expect an adaptive selection. TrackGroupArray trackGroups = singleTrackGroup(buildVideoFormat("0"), buildVideoFormat("1")); trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setAllowVideoNonSeamlessAdaptiveness(true)); + defaultParameters.buildUpon().setAllowVideoNonSeamlessAdaptiveness(true)); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {nonSeamlessVideoCapabilities}, @@ -1399,7 +1462,7 @@ public final class DefaultTrackSelectorTest { // If we explicitly disable non-seamless adaptiveness, expect a fixed selection. trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setAllowVideoNonSeamlessAdaptiveness(false)); + defaultParameters.buildUpon().setAllowVideoNonSeamlessAdaptiveness(false)); result = trackSelector.selectTracks( new RendererCapabilities[] {nonSeamlessVideoCapabilities}, @@ -1434,7 +1497,7 @@ public final class DefaultTrackSelectorTest { // If we explicitly enable mixed mime type adaptiveness, expect an adaptive selection. trackSelector.setParameters( - Parameters.DEFAULT.buildUpon().setAllowVideoMixedMimeTypeAdaptiveness(true)); + defaultParameters.buildUpon().setAllowVideoMixedMimeTypeAdaptiveness(true)); result = trackSelector.selectTracks( new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1691,13 +1754,13 @@ public final class DefaultTrackSelectorTest { } @Override - public int supportsFormat(Format format) throws ExoPlaybackException { + public int supportsFormat(Format format) { return MimeTypes.getTrackType(format.sampleMimeType) == trackType ? (supportValue) : FORMAT_UNSUPPORTED_TYPE; } @Override - public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + public int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_SEAMLESS; } @@ -1732,14 +1795,14 @@ public final class DefaultTrackSelectorTest { } @Override - public int supportsFormat(Format format) throws ExoPlaybackException { + public int supportsFormat(Format format) { return format.id != null && formatToCapability.containsKey(format.id) ? formatToCapability.get(format.id) : FORMAT_UNSUPPORTED_TYPE; } @Override - public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + public int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_SEAMLESS; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java index 2426073d8a..1eb49188bf 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java @@ -107,7 +107,8 @@ public class BaseDataSourceTest { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { throw new UnsupportedOperationException(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java index 2df9a608e9..8cb142f05d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.fail; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import org.junit.Before; @@ -31,6 +32,9 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class DataSchemeDataSourceTest { + private static final String DATA_SCHEME_URI = + "data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiLCJjb250ZW50X2lkIjoiTWpBeE5WOTBaV" + + "0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiXX0="; private DataSource schemeDataDataSource; @Before @@ -40,9 +44,7 @@ public final class DataSchemeDataSourceTest { @Test public void testBase64Data() throws IOException { - DataSpec dataSpec = buildDataSpec("data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiL" - + "CJjb250ZW50X2lkIjoiTWpBeE5WOTBaV0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwM" - + "DAwMDAwMDAwMDAiXX0="); + DataSpec dataSpec = buildDataSpec(DATA_SCHEME_URI); DataSourceAsserts.assertDataSourceContent( schemeDataDataSource, dataSpec, @@ -72,6 +74,52 @@ public final class DataSchemeDataSourceTest { assertThat(Util.fromUtf8Bytes(buffer, 0, 18)).isEqualTo("012345678901234567"); } + @Test + public void testSequentialRangeRequests() throws IOException { + DataSpec dataSpec = + buildDataSpec(DATA_SCHEME_URI, /* position= */ 1, /* length= */ C.LENGTH_UNSET); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, + dataSpec, + Util.getUtf8Bytes( + "\"provider\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + + "[\"00000000000000000000000000000000\"]}")); + dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 10, /* length= */ C.LENGTH_UNSET); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, + dataSpec, + Util.getUtf8Bytes( + "\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + + "[\"00000000000000000000000000000000\"]}")); + dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 15, /* length= */ 5); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, dataSpec, Util.getUtf8Bytes("devin")); + } + + @Test + public void testInvalidStartPositionRequest() throws IOException { + try { + // Try to open a range starting one byte beyond the resource's length. + schemeDataDataSource.open( + buildDataSpec(DATA_SCHEME_URI, /* position= */ 108, /* length= */ C.LENGTH_UNSET)); + fail(); + } catch (DataSourceException e) { + assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE); + } + } + + @Test + public void testRangeExceedingResourceLengthRequest() throws IOException { + try { + // Try to open a range exceeding the resource's length. + schemeDataDataSource.open( + buildDataSpec(DATA_SCHEME_URI, /* position= */ 97, /* length= */ 11)); + fail(); + } catch (DataSourceException e) { + assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE); + } + } + @Test public void testIncorrectScheme() { try { @@ -99,7 +147,11 @@ public final class DataSchemeDataSourceTest { } private static DataSpec buildDataSpec(String uriString) { - return new DataSpec(Uri.parse(uriString)); + return buildDataSpec(uriString, /* position= */ 0, /* length= */ C.LENGTH_UNSET); + } + + private static DataSpec buildDataSpec(String uriString, int position, int length) { + return new DataSpec(Uri.parse(uriString), position, length, /* key= */ null); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java index 257f1c45b3..e9823697f7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java @@ -40,7 +40,8 @@ public final class DataSourceInputStreamTest { // Read bytes. for (int i = 0; i < TEST_DATA.length; i++) { int readByte = inputStream.read(); - assertThat(0 <= readByte && readByte < 256).isTrue(); + assertThat(0 <= readByte).isTrue(); + assertThat(readByte < 256).isTrue(); assertThat(readByte).isEqualTo(TEST_DATA[i] & 0xFF); assertThat(inputStream.bytesRead()).isEqualTo(i + 1); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 956a5fc283..83104119ad 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -363,7 +363,7 @@ public final class CacheDataSourceTest { .appendReadData(1); // Lock the content on the cache. - SimpleCacheSpan cacheSpan = cache.startReadWriteNonBlocking(defaultCacheKey, 0); + CacheSpan cacheSpan = cache.startReadWriteNonBlocking(defaultCacheKey, 0); assertThat(cacheSpan).isNotNull(); assertThat(cacheSpan.isHoleSpan()).isTrue(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index b00ee73f0f..73780f56f3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import androidx.test.core.app.ApplicationProvider; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 3d684aab82..fc229d9dc6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -164,7 +164,7 @@ public class SimpleCacheTest { .isEqualTo(150); // Removing the last span shouldn't cause the length be change next time cache loaded - SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145); + CacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145); simpleCache2.removeSpan(lastSpan); simpleCache2.release(); simpleCache2 = getSimpleCache(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java index 0392f8b26d..2a1c59e7df 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java @@ -28,7 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for ColorParser. */ +/** Unit test for {@link ColorParser}. */ @RunWith(AndroidJUnit4.class) public final class ColorParserTest { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java new file mode 100644 index 0000000000..72a80161f2 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java @@ -0,0 +1,87 @@ +/* + * 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.util; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link FlacStreamMetadata}. */ +@RunWith(AndroidJUnit4.class) +public final class FlacStreamMetadataTest { + + @Test + public void parseVorbisComments() { + ArrayList commentsList = new ArrayList<>(); + commentsList.add("Title=Song"); + commentsList.add("Artist=Singer"); + + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; + + assertThat(metadata.length()).isEqualTo(2); + VorbisComment commentFrame = (VorbisComment) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("Song"); + commentFrame = (VorbisComment) metadata.get(1); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Singer"); + } + + @Test + public void parseEmptyVorbisComments() { + ArrayList commentsList = new ArrayList<>(); + + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; + + assertThat(metadata).isNull(); + } + + @Test + public void parseVorbisCommentWithEqualsInValue() { + ArrayList commentsList = new ArrayList<>(); + commentsList.add("Title=So=ng"); + + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; + + assertThat(metadata.length()).isEqualTo(1); + VorbisComment commentFrame = (VorbisComment) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("So=ng"); + } + + @Test + public void parseInvalidVorbisComment() { + ArrayList commentsList = new ArrayList<>(); + commentsList.add("TitleSong"); + commentsList.add("Artist=Singer"); + + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; + + assertThat(metadata.length()).isEqualTo(1); + VorbisComment commentFrame = (VorbisComment) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Singer"); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 9abec0cd8f..5a13ed0dd8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -268,14 +268,19 @@ public class UtilTest { @Test @Config(sdk = 21) public void testNormalizeLanguageCodeV21() { - assertThat(Util.normalizeLanguageCode("es")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("spa-ar"); - assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("spa-ar"); - assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("spa-ar-dialect"); - assertThat(Util.normalizeLanguageCode("es-419")).isEqualTo("spa-419"); - assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zho-hans-tw"); - assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zho-tw"); + assertThat(Util.normalizeLanguageCode(null)).isNull(); + assertThat(Util.normalizeLanguageCode("")).isEmpty(); + assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); + assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); + assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); + assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zh-tw"); + assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); } @@ -283,13 +288,49 @@ public class UtilTest { @Test @Config(sdk = 16) public void testNormalizeLanguageCode() { - assertThat(Util.normalizeLanguageCode("es")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("spa"); + assertThat(Util.normalizeLanguageCode(null)).isNull(); + assertThat(Util.normalizeLanguageCode("")).isEmpty(); + assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); + assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); + assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); + // Doesn't work on API < 21 because we can't use Locale syntax verification. + // assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zh-tw"); + assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); } + @Test + public void testNormalizeIso6392BibliographicalAndTextualCodes() { + // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. + assertThat(Util.normalizeLanguageCode("alb")).isEqualTo(Util.normalizeLanguageCode("sqi")); + assertThat(Util.normalizeLanguageCode("arm")).isEqualTo(Util.normalizeLanguageCode("hye")); + assertThat(Util.normalizeLanguageCode("baq")).isEqualTo(Util.normalizeLanguageCode("eus")); + assertThat(Util.normalizeLanguageCode("bur")).isEqualTo(Util.normalizeLanguageCode("mya")); + assertThat(Util.normalizeLanguageCode("chi")).isEqualTo(Util.normalizeLanguageCode("zho")); + assertThat(Util.normalizeLanguageCode("cze")).isEqualTo(Util.normalizeLanguageCode("ces")); + assertThat(Util.normalizeLanguageCode("dut")).isEqualTo(Util.normalizeLanguageCode("nld")); + assertThat(Util.normalizeLanguageCode("fre")).isEqualTo(Util.normalizeLanguageCode("fra")); + assertThat(Util.normalizeLanguageCode("geo")).isEqualTo(Util.normalizeLanguageCode("kat")); + assertThat(Util.normalizeLanguageCode("ger")).isEqualTo(Util.normalizeLanguageCode("deu")); + assertThat(Util.normalizeLanguageCode("gre")).isEqualTo(Util.normalizeLanguageCode("ell")); + assertThat(Util.normalizeLanguageCode("ice")).isEqualTo(Util.normalizeLanguageCode("isl")); + assertThat(Util.normalizeLanguageCode("mac")).isEqualTo(Util.normalizeLanguageCode("mkd")); + assertThat(Util.normalizeLanguageCode("mao")).isEqualTo(Util.normalizeLanguageCode("mri")); + assertThat(Util.normalizeLanguageCode("may")).isEqualTo(Util.normalizeLanguageCode("msa")); + assertThat(Util.normalizeLanguageCode("per")).isEqualTo(Util.normalizeLanguageCode("fas")); + assertThat(Util.normalizeLanguageCode("rum")).isEqualTo(Util.normalizeLanguageCode("ron")); + assertThat(Util.normalizeLanguageCode("slo")).isEqualTo(Util.normalizeLanguageCode("slk")); + assertThat(Util.normalizeLanguageCode("tib")).isEqualTo(Util.normalizeLanguageCode("bod")); + assertThat(Util.normalizeLanguageCode("wel")).isEqualTo(Util.normalizeLanguageCode("cym")); + } + private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); diff --git a/library/dash/build.gradle b/library/dash/build.gradle index f6981a2220..c64da2b86d 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -41,8 +41,10 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.2' - testImplementation project(modulePrefix + 'testutils-robolectric') + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion + implementation 'androidx.annotation:annotation:1.1.0' + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index 40d4e468bd..f7edf62182 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -69,4 +69,11 @@ public interface DashChunkSource extends ChunkSource { * @param newManifest The new manifest. */ void updateManifest(DashManifest newManifest, int periodIndex); + + /** + * Updates the track selection. + * + * @param trackSelection The new track selection instance. Must be equivalent to the previous one. + */ + void updateTrackSelection(TrackSelection trackSelection); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index aa080bbdec..21fd43da21 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -22,6 +22,8 @@ import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.EmptySampleStream; @@ -58,6 +60,7 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** A DASH {@link MediaPeriod}. */ /* package */ final class DashMediaPeriod @@ -69,7 +72,8 @@ import java.util.regex.Pattern; /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long elapsedRealtimeOffsetMs; private final LoaderErrorThrower manifestLoaderErrorThrower; @@ -82,7 +86,7 @@ import java.util.regex.Pattern; trackEmsgHandlerBySampleStream; private final EventDispatcher eventDispatcher; - private @Nullable Callback callback; + @Nullable private Callback callback; private ChunkSampleStream[] sampleStreams; private EventSampleStream[] eventSampleStreams; private SequenceableLoader compositeSequenceableLoader; @@ -97,6 +101,7 @@ import java.util.regex.Pattern; int periodIndex, DashChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, long elapsedRealtimeOffsetMs, @@ -109,6 +114,7 @@ import java.util.regex.Pattern; this.periodIndex = periodIndex; this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; @@ -123,8 +129,8 @@ import java.util.regex.Pattern; compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); Period period = manifest.getPeriod(periodIndex); eventStreams = period.eventStreams; - Pair result = buildTrackGroups(period.adaptationSets, - eventStreams); + Pair result = + buildTrackGroups(drmSessionManager, period.adaptationSets, eventStreams); trackGroups = result.first; trackGroupInfos = result.second; eventDispatcher.mediaPeriodCreated(); @@ -240,8 +246,12 @@ import java.util.regex.Pattern; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { int[] streamIndexToTrackGroupIndex = getStreamIndexToTrackGroupIndex(selections); releaseDisabledStreams(selections, mayRetainStreamFlags, streams); releaseOrphanEmbeddedStreams(selections, streams, streamIndexToTrackGroupIndex); @@ -401,17 +411,27 @@ import java.util.regex.Pattern; int[] streamIndexToTrackGroupIndex) { // Create newly selected primary and event streams. for (int i = 0; i < selections.length; i++) { - if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + if (selection == null) { + continue; + } + if (streams[i] == null) { + // Create new stream for selection. streamResetFlags[i] = true; int trackGroupIndex = streamIndexToTrackGroupIndex[i]; TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) { - streams[i] = buildSampleStream(trackGroupInfo, selections[i], positionUs); + streams[i] = buildSampleStream(trackGroupInfo, selection, positionUs); } else if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) { EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex); - Format format = selections[i].getTrackGroup().getFormat(0); + Format format = selection.getTrackGroup().getFormat(0); streams[i] = new EventSampleStream(eventStream, format, manifest.dynamic); } + } else if (streams[i] instanceof ChunkSampleStream) { + // Update selection in existing stream. + @SuppressWarnings("unchecked") + ChunkSampleStream stream = (ChunkSampleStream) streams[i]; + stream.getChunkSource().updateTrackSelection(selection); } } // Create newly selected embedded streams from the corresponding primary stream. Note that this @@ -455,7 +475,9 @@ import java.util.regex.Pattern; } private static Pair buildTrackGroups( - List adaptationSets, List eventStreams) { + DrmSessionManager drmSessionManager, + List adaptationSets, + List eventStreams) { int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); int primaryGroupCount = groupedAdaptationSetIndices.length; @@ -475,6 +497,7 @@ import java.util.regex.Pattern; int trackGroupCount = buildPrimaryAndEmbeddedTrackGroupInfos( + drmSessionManager, adaptationSets, groupedAdaptationSetIndices, primaryGroupCount, @@ -569,6 +592,7 @@ import java.util.regex.Pattern; } private static int buildPrimaryAndEmbeddedTrackGroupInfos( + DrmSessionManager drmSessionManager, List adaptationSets, int[][] groupedAdaptationSetIndices, int primaryGroupCount, @@ -585,7 +609,14 @@ import java.util.regex.Pattern; } Format[] formats = new Format[representations.size()]; for (int j = 0; j < formats.length; j++) { - formats[j] = representations.get(j).format; + Format format = representations.get(j).format; + DrmInitData drmInitData = format.drmInitData; + if (drmInitData != null) { + format = + format.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(drmInitData)); + } + formats[j] = format; } AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]); @@ -692,6 +723,7 @@ import java.util.regex.Pattern; this, allocator, positionUs, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher); synchronized (this) { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 709fd00ea7..890a272c5e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -25,6 +25,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.FilteringManifestParser; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BaseMediaSource; @@ -79,6 +81,7 @@ public final class DashMediaSource extends BaseMediaSource { private final DashChunkSource.Factory chunkSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory; + private DrmSessionManager drmSessionManager; @Nullable private ParsingLoadable.Parser manifestParser; @Nullable private List streamKeys; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; @@ -112,6 +115,7 @@ public final class DashMediaSource extends BaseMediaSource { @Nullable DataSource.Factory manifestDataSourceFactory) { this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); @@ -126,12 +130,26 @@ public final class DashMediaSource extends BaseMediaSource { * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setTag(Object tag) { + public Factory setTag(@Nullable Object tag) { Assertions.checkState(!isCreateCalled); this.tag = tag; return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. See {@link * #setLoadErrorHandlingPolicy} for the default value. @@ -253,6 +271,7 @@ public final class DashMediaSource extends BaseMediaSource { /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest, @@ -313,6 +332,7 @@ public final class DashMediaSource extends BaseMediaSource { manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest, @@ -346,8 +366,8 @@ public final class DashMediaSource extends BaseMediaSource { /** * The interval in milliseconds between invocations of {@link - * SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the - * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams). + * MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} when the source's {@link + * Timeline} is changing dynamically (for example, for incomplete live streams). */ private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; /** @@ -361,6 +381,7 @@ public final class DashMediaSource extends BaseMediaSource { private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long livePresentationDelayMs; private final boolean livePresentationDelayOverridesManifest; @@ -373,11 +394,11 @@ public final class DashMediaSource extends BaseMediaSource { private final Runnable simulateManifestRefreshRunnable; private final PlayerEmsgCallback playerEmsgCallback; private final LoaderErrorThrower manifestLoadErrorThrower; - private final @Nullable Object tag; + @Nullable private final Object tag; private DataSource dataSource; private Loader loader; - private @Nullable TransferListener mediaTransferListener; + @Nullable private TransferListener mediaTransferListener; private IOException manifestFatalError; private Handler handler; @@ -409,8 +430,8 @@ public final class DashMediaSource extends BaseMediaSource { public DashMediaSource( DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, chunkSourceFactory, @@ -434,8 +455,8 @@ public final class DashMediaSource extends BaseMediaSource { DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, /* manifestUri= */ null, @@ -443,6 +464,7 @@ public final class DashMediaSource extends BaseMediaSource { /* manifestParser= */ null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), DEFAULT_LIVE_PRESENTATION_DELAY_MS, /* livePresentationDelayOverridesManifest= */ false, @@ -470,8 +492,8 @@ public final class DashMediaSource extends BaseMediaSource { Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifestUri, manifestDataSourceFactory, @@ -507,8 +529,8 @@ public final class DashMediaSource extends BaseMediaSource { DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifestUri, manifestDataSourceFactory, @@ -547,8 +569,8 @@ public final class DashMediaSource extends BaseMediaSource { DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( /* manifest= */ null, manifestUri, @@ -556,6 +578,7 @@ public final class DashMediaSource extends BaseMediaSource { manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), livePresentationDelayMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS ? DEFAULT_LIVE_PRESENTATION_DELAY_MS @@ -568,12 +591,13 @@ public final class DashMediaSource extends BaseMediaSource { } private DashMediaSource( - DashManifest manifest, - Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, - ParsingLoadable.Parser manifestParser, + @Nullable DashManifest manifest, + @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + @Nullable ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, boolean livePresentationDelayOverridesManifest, @@ -584,6 +608,7 @@ public final class DashMediaSource extends BaseMediaSource { this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.livePresentationDelayMs = livePresentationDelayMs; this.livePresentationDelayOverridesManifest = livePresentationDelayOverridesManifest; @@ -630,7 +655,7 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; if (sideloadedManifest) { processManifest(false); @@ -660,6 +685,7 @@ public final class DashMediaSource extends BaseMediaSource { periodIndex, chunkSourceFactory, mediaTransferListener, + drmSessionManager, loadErrorHandlingPolicy, periodEventDispatcher, elapsedRealtimeOffsetMs, @@ -679,7 +705,7 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { manifestLoadPending = false; dataSource = null; if (loader != null) { @@ -994,7 +1020,7 @@ public final class DashMediaSource extends BaseMediaSource { windowDefaultStartPositionUs, manifest, tag); - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); if (!sideloadedManifest) { // Remove any pending simulated refresh. @@ -1139,7 +1165,7 @@ public final class DashMediaSource extends BaseMediaSource { private final long windowDurationUs; private final long windowDefaultStartPositionUs; private final DashManifest manifest; - private final @Nullable Object windowTag; + @Nullable private final Object windowTag; public DashTimeline( long presentationStartTimeMs, @@ -1193,6 +1219,7 @@ public final class DashMediaSource extends BaseMediaSource { && manifest.durationMs == C.TIME_UNSET; return window.set( tag, + manifest, presentationStartTimeMs, windowStartTimeMs, /* isSeekable= */ true, diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java index 6a6e08ce1d..c9433b9e41 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -66,7 +66,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static @Nullable DrmInitData loadDrmInitData(DataSource dataSource, Period period) + @Nullable + public static DrmInitData loadDrmInitData(DataSource dataSource, Period period) throws IOException, InterruptedException { int primaryTrackType = C.TRACK_TYPE_VIDEO; Representation representation = getFirstRepresentation(period, primaryTrackType); @@ -95,7 +96,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static @Nullable Format loadSampleFormat( + @Nullable + public static Format loadSampleFormat( DataSource dataSource, int trackType, Representation representation) throws IOException, InterruptedException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, @@ -116,7 +118,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static @Nullable ChunkIndex loadChunkIndex( + @Nullable + public static ChunkIndex loadChunkIndex( DataSource dataSource, int trackType, Representation representation) throws IOException, InterruptedException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, @@ -138,7 +141,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - private static @Nullable ChunkExtractorWrapper loadInitializationData( + @Nullable + private static ChunkExtractorWrapper loadInitializationData( DataSource dataSource, int trackType, Representation representation, boolean loadIndex) throws IOException, InterruptedException { RangedUri initializationUri = representation.getInitializationUri(); @@ -187,7 +191,8 @@ public final class DashUtil { return new ChunkExtractorWrapper(extractor, trackType, format); } - private static @Nullable Representation getFirstRepresentation(Period period, int type) { + @Nullable + private static Representation getFirstRepresentation(Period period, int type) { int index = period.getAdaptationSetIndex(type); if (index == C.INDEX_UNSET) { return null; @@ -197,5 +202,4 @@ public final class DashUtil { } private DashUtil() {} - } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 02b2990193..cd39c9538a 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -67,7 +67,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private final int maxSegmentsPerLoad; public Factory(DataSource.Factory dataSourceFactory) { - this(dataSourceFactory, 1); + this(dataSourceFactory, /* maxSegmentsPerLoad= */ 1); } public Factory(DataSource.Factory dataSourceFactory, int maxSegmentsPerLoad) { @@ -111,7 +111,6 @@ public class DefaultDashChunkSource implements DashChunkSource { private final LoaderErrorThrower manifestLoaderErrorThrower; private final int[] adaptationSetIndices; - private final TrackSelection trackSelection; private final int trackType; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; @@ -120,6 +119,7 @@ public class DefaultDashChunkSource implements DashChunkSource { protected final RepresentationHolder[] representationHolders; + private TrackSelection trackSelection; private DashManifest manifest; private int periodIndex; private IOException fatalError; @@ -222,6 +222,11 @@ public class DefaultDashChunkSource implements DashChunkSource { } } + @Override + public void updateTrackSelection(TrackSelection trackSelection) { + this.trackSelection = trackSelection; + } + @Override public void maybeThrowError() throws IOException { if (fatalError != null) { @@ -329,6 +334,7 @@ public class DefaultDashChunkSource implements DashChunkSource { representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); long lastAvailableSegmentNum = representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); + updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum); long segmentNum = @@ -616,7 +622,7 @@ public class DefaultDashChunkSource implements DashChunkSource { /* package */ final @Nullable ChunkExtractorWrapper extractorWrapper; public final Representation representation; - public final @Nullable DashSegmentIndex segmentIndex; + @Nullable public final DashSegmentIndex segmentIndex; private final long periodDurationUs; private final long segmentNumShift; @@ -627,7 +633,7 @@ public class DefaultDashChunkSource implements DashChunkSource { Representation representation, boolean enableEventMessageTrack, List closedCaptionFormats, - TrackOutput playerEmsgTrackOutput) { + @Nullable TrackOutput playerEmsgTrackOutput) { this( periodDurationUs, representation, @@ -781,7 +787,7 @@ public class DefaultDashChunkSource implements DashChunkSource { Representation representation, boolean enableEventMessageTrack, List closedCaptionFormats, - TrackOutput playerEmsgTrackOutput) { + @Nullable TrackOutput playerEmsgTrackOutput) { String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { return null; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index 34e1ecc2b6..af4bf3ad70 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -371,7 +371,14 @@ public final class PlayerEmsgHandler implements Handler.Callback { @Nullable private MetadataInputBuffer dequeueSample() { buffer.clear(); - int result = sampleQueue.read(formatHolder, buffer, false, false, 0); + int result = + sampleQueue.read( + formatHolder, + buffer, + /* formatRequired= */ false, + /* allowOnlyClearBuffers= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); if (result == C.RESULT_BUFFER_READ) { buffer.flip(); return buffer; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 0c3f641cbe..2d8909f8b4 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -80,12 +80,10 @@ public class DashManifest implements FilterableManifest { * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section * 4.7.2. */ - public final UtcTimingElement utcTiming; + @Nullable public final UtcTimingElement utcTiming; - /** - * The location of this manifest. - */ - public final Uri location; + /** The location of this manifest, or null if not present. */ + @Nullable public final Uri location; /** The {@link ProgramInformation}, or null if not present. */ @Nullable public final ProgramInformation programInformation; @@ -106,8 +104,8 @@ public class DashManifest implements FilterableManifest { long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, long publishTimeMs, - UtcTimingElement utcTiming, - Uri location, + @Nullable UtcTimingElement utcTiming, + @Nullable Uri location, List periods) { this( availabilityStartTimeMs, @@ -134,8 +132,8 @@ public class DashManifest implements FilterableManifest { long suggestedPresentationDelayMs, long publishTimeMs, @Nullable ProgramInformation programInformation, - UtcTimingElement utcTiming, - Uri location, + @Nullable UtcTimingElement utcTiming, + @Nullable Uri location, List periods) { this.availabilityStartTimeMs = availabilityStartTimeMs; this.durationMs = durationMs; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 912f7d1611..8affcb27ce 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; +import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Base64; import android.util.Pair; @@ -42,10 +43,12 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.compatqual.NullableType; import org.xml.sax.helpers.DefaultHandler; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -188,9 +191,9 @@ public class DashManifestParser extends DefaultHandler long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, long publishTimeMs, - ProgramInformation programInformation, - UtcTimingElement utcTiming, - Uri location, + @Nullable ProgramInformation programInformation, + @Nullable UtcTimingElement utcTiming, + @Nullable Uri location, List periods) { return new DashManifest( availabilityStartTime, @@ -242,7 +245,7 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { segmentBase = parseSegmentList(xpp, null); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, null,null); + segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList()); } else { maybeSkipTag(xpp); } @@ -258,8 +261,9 @@ public class DashManifestParser extends DefaultHandler // AdaptationSet parsing. - protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, - SegmentBase segmentBase) throws XmlPullParserException, IOException { + protected AdaptationSet parseAdaptationSet( + XmlPullParser xpp, String baseUrl, @Nullable SegmentBase segmentBase) + throws XmlPullParserException, IOException { int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET); int contentType = parseContentType(xpp); @@ -333,8 +337,8 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, - supplementalProperties); + segmentBase = + parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, supplementalProperties); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp)) { @@ -393,8 +397,8 @@ public class DashManifestParser extends DefaultHandler * @return The scheme type and/or {@link SchemeData} parsed from the ContentProtection element. * Either or both may be null, depending on the ContentProtection element being parsed. */ - protected Pair parseContentProtection(XmlPullParser xpp) - throws XmlPullParserException, IOException { + protected Pair<@NullableType String, @NullableType SchemeData> parseContentProtection( + XmlPullParser xpp) throws XmlPullParserException, IOException { String schemeType = null; String licenseServerUrl = null; byte[] data = null; @@ -476,19 +480,19 @@ public class DashManifestParser extends DefaultHandler protected RepresentationInfo parseRepresentation( XmlPullParser xpp, String baseUrl, - String label, - String adaptationSetMimeType, - String adaptationSetCodecs, + @Nullable String label, + @Nullable String adaptationSetMimeType, + @Nullable String adaptationSetCodecs, int adaptationSetWidth, int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels, int adaptationSetAudioSamplingRate, - String adaptationSetLanguage, + @Nullable String adaptationSetLanguage, List adaptationSetRoleDescriptors, List adaptationSetAccessibilityDescriptors, List adaptationSetSupplementalProperties, - SegmentBase segmentBase) + @Nullable SegmentBase segmentBase) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -520,8 +524,9 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, - adaptationSetSupplementalProperties); + segmentBase = + parseSegmentTemplate( + xpp, (SegmentTemplate) segmentBase, adaptationSetSupplementalProperties); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { Pair contentProtection = parseContentProtection(xpp); if (contentProtection.first != null) { @@ -562,19 +567,19 @@ public class DashManifestParser extends DefaultHandler } protected Format buildFormat( - String id, - String label, - String containerMimeType, + @Nullable String id, + @Nullable String label, + @Nullable String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, - String language, + @Nullable String language, List roleDescriptors, List accessibilityDescriptors, - String codecs, + @Nullable String codecs, List supplementalProperties) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); @C.SelectionFlags int selectionFlags = parseSelectionFlagsFromRoleDescriptors(roleDescriptors); @@ -648,7 +653,7 @@ public class DashManifestParser extends DefaultHandler protected Representation buildRepresentation( RepresentationInfo representationInfo, - String extraDrmSchemeType, + @Nullable String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, ArrayList extraInbandEventStreams) { Format format = representationInfo.format; @@ -673,7 +678,8 @@ public class DashManifestParser extends DefaultHandler // SegmentBase, SegmentList and SegmentTemplate parsing. - protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, SingleSegmentBase parent) + protected SingleSegmentBase parseSegmentBase( + XmlPullParser xpp, @Nullable SingleSegmentBase parent) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -709,7 +715,7 @@ public class DashManifestParser extends DefaultHandler indexLength); } - protected SegmentList parseSegmentList(XmlPullParser xpp, SegmentList parent) + protected SegmentList parseSegmentList(XmlPullParser xpp, @Nullable SegmentList parent) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -754,13 +760,15 @@ public class DashManifestParser extends DefaultHandler long presentationTimeOffset, long startNumber, long duration, - List timeline, - List segments) { + @Nullable List timeline, + @Nullable List segments) { return new SegmentList(initialization, timescale, presentationTimeOffset, startNumber, duration, timeline, segments); } - protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, SegmentTemplate parent, + protected SegmentTemplate parseSegmentTemplate( + XmlPullParser xpp, + @Nullable SegmentTemplate parent, List adaptationSetSupplementalProperties) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -768,6 +776,9 @@ public class DashManifestParser extends DefaultHandler parent != null ? parent.presentationTimeOffset : 0); long duration = parseLong(xpp, "duration", parent != null ? parent.duration : C.TIME_UNSET); long startNumber = parseLong(xpp, "startNumber", parent != null ? parent.startNumber : 1); + long endNumber = + parseLastSegmentNumberSupplementalProperty(adaptationSetSupplementalProperties); + UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media", parent != null ? parent.mediaTemplate : null); UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization", @@ -792,27 +803,16 @@ public class DashManifestParser extends DefaultHandler timeline = timeline != null ? timeline : parent.segmentTimeline; } - long endNumber = C.INDEX_UNSET; - - if (adaptationSetSupplementalProperties != null) { - endNumber = parseLastSegmentNumberSupplementalProperty - (adaptationSetSupplementalProperties); - } - - return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, initializationTemplate, mediaTemplate, - endNumber); - } - - protected long parseLastSegmentNumberSupplementalProperty - (List supplementalProperties){ - for (Descriptor descriptor : supplementalProperties) { - if ("http://dashif.org/guidelines/last-segment-number" - .equalsIgnoreCase(descriptor.schemeIdUri)) { - return Long.parseLong(descriptor.value); - } - } - return C.INDEX_UNSET; + return buildSegmentTemplate( + initialization, + timescale, + presentationTimeOffset, + startNumber, + endNumber, + duration, + timeline, + initializationTemplate, + mediaTemplate); } protected SegmentTemplate buildSegmentTemplate( @@ -820,18 +820,21 @@ public class DashManifestParser extends DefaultHandler long timescale, long presentationTimeOffset, long startNumber, + long endNumber, long duration, List timeline, - UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate,long endNumber ) { - - if (endNumber!=C.INDEX_UNSET) { - return new SegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber, endNumber,duration, timeline, initializationTemplate, mediaTemplate); - } - - return new SegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber,duration, timeline, initializationTemplate, mediaTemplate); + @Nullable UrlTemplate initializationTemplate, + @Nullable UrlTemplate mediaTemplate) { + return new SegmentTemplate( + initialization, + timescale, + presentationTimeOffset, + startNumber, + endNumber, + duration, + timeline, + initializationTemplate, + mediaTemplate); } /** @@ -900,7 +903,7 @@ public class DashManifestParser extends DefaultHandler long id = parseLong(xpp, "id", 0); long duration = parseLong(xpp, "duration", C.TIME_UNSET); long presentationTime = parseLong(xpp, "presentationTime", 0); - long durationMs = Util.scaleLargeTimestamp(duration, 1000, timescale); + long durationMs = Util.scaleLargeTimestamp(duration, C.MILLIS_PER_SECOND, timescale); long presentationTimesUs = Util.scaleLargeTimestamp(presentationTime, C.MICROS_PER_SECOND, timescale); String messageData = parseString(xpp, "messageData", null); @@ -1009,8 +1012,9 @@ public class DashManifestParser extends DefaultHandler return new SegmentTimelineElement(elapsedTime, duration); } - protected UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name, - UrlTemplate defaultValue) { + @Nullable + protected UrlTemplate parseUrlTemplate( + XmlPullParser xpp, String name, @Nullable UrlTemplate defaultValue) { String valueString = xpp.getAttributeValue(null, name); if (valueString != null) { return UrlTemplate.compile(valueString); @@ -1127,7 +1131,7 @@ public class DashManifestParser extends DefaultHandler } @C.RoleFlags - protected int parseDashRoleSchemeValue(String value) { + protected int parseDashRoleSchemeValue(@Nullable String value) { if (value == null) { return 0; } @@ -1160,7 +1164,7 @@ public class DashManifestParser extends DefaultHandler } @C.RoleFlags - protected int parseTvaAudioPurposeCsValue(String value) { + protected int parseTvaAudioPurposeCsValue(@Nullable String value) { if (value == null) { return 0; } @@ -1231,7 +1235,9 @@ public class DashManifestParser extends DefaultHandler * @param codecs The codecs attribute. * @return The derived sample mimeType, or null if it could not be derived. */ - private static String getSampleMimeType(String containerMimeType, String codecs) { + @Nullable + private static String getSampleMimeType( + @Nullable String containerMimeType, @Nullable String codecs) { if (MimeTypes.isAudio(containerMimeType)) { return MimeTypes.getAudioMediaMimeType(codecs); } else if (MimeTypes.isVideo(containerMimeType)) { @@ -1265,7 +1271,7 @@ public class DashManifestParser extends DefaultHandler * @param mimeType The mimeType. * @return Whether the mimeType is a text sample mimeType. */ - private static boolean mimeTypeIsRawText(String mimeType) { + private static boolean mimeTypeIsRawText(@Nullable String mimeType) { return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType) || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) @@ -1274,16 +1280,18 @@ public class DashManifestParser extends DefaultHandler } /** - * Checks two languages for consistency, returning the consistent language, or throwing an - * {@link IllegalStateException} if the languages are inconsistent. - *

- * Two languages are consistent if they are equal, or if one is null. + * Checks two languages for consistency, returning the consistent language, or throwing an {@link + * IllegalStateException} if the languages are inconsistent. + * + *

Two languages are consistent if they are equal, or if one is null. * * @param firstLanguage The first language. * @param secondLanguage The second language. * @return The consistent language. */ - private static String checkLanguageConsistency(String firstLanguage, String secondLanguage) { + @Nullable + private static String checkLanguageConsistency( + @Nullable String firstLanguage, @Nullable String secondLanguage) { if (firstLanguage == null) { return secondLanguage; } else if (secondLanguage == null) { @@ -1468,20 +1476,37 @@ public class DashManifestParser extends DefaultHandler } } + protected static long parseLastSegmentNumberSupplementalProperty( + List supplementalProperties) { + for (int i = 0; i < supplementalProperties.size(); i++) { + Descriptor descriptor = supplementalProperties.get(i); + if ("http://dashif.org/guidelines/last-segment-number" + .equalsIgnoreCase(descriptor.schemeIdUri)) { + return Long.parseLong(descriptor.value); + } + } + return C.INDEX_UNSET; + } + /** A parsed Representation element. */ protected static final class RepresentationInfo { public final Format format; public final String baseUrl; public final SegmentBase segmentBase; - public final String drmSchemeType; + @Nullable public final String drmSchemeType; public final ArrayList drmSchemeDatas; public final ArrayList inbandEventStreams; public final long revisionId; - public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, - String drmSchemeType, ArrayList drmSchemeDatas, - ArrayList inbandEventStreams, long revisionId) { + public RepresentationInfo( + Format format, + String baseUrl, + SegmentBase segmentBase, + @Nullable String drmSchemeType, + ArrayList drmSchemeDatas, + ArrayList inbandEventStreams, + long revisionId) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java index 493a8da09c..d68690d363 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.source.dash.manifest; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; @@ -24,10 +23,8 @@ import com.google.android.exoplayer2.util.Util; */ public final class Descriptor { - /** - * The scheme URI. - */ - @NonNull public final String schemeIdUri; + /** The scheme URI. */ + public final String schemeIdUri; /** * The value, or null. */ @@ -42,7 +39,7 @@ public final class Descriptor { * @param value The value, or null. * @param id The identifier, or null. */ - public Descriptor(@NonNull String schemeIdUri, @Nullable String value, @Nullable String id) { + public Descriptor(String schemeIdUri, @Nullable String value, @Nullable String id) { this.schemeIdUri = schemeIdUri; this.value = value; this.id = id; @@ -63,10 +60,9 @@ public final class Descriptor { @Override public int hashCode() { - int result = (schemeIdUri != null ? schemeIdUri.hashCode() : 0); + int result = schemeIdUri.hashCode(); result = 31 * result + (value != null ? value.hashCode() : 0); result = 31 * result + (id != null ? id.hashCode() : 0); return result; } - } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java index 62934d7433..ac264bd2b1 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java @@ -21,22 +21,26 @@ import com.google.android.exoplayer2.util.Util; /** A parsed program information element. */ public class ProgramInformation { /** The title for the media presentation. */ - public final String title; + @Nullable public final String title; /** Information about the original source of the media presentation. */ - public final String source; + @Nullable public final String source; /** A copyright statement for the media presentation. */ - public final String copyright; + @Nullable public final String copyright; /** A URL that provides more information about the media presentation. */ - public final String moreInformationURL; + @Nullable public final String moreInformationURL; /** Declares the language code(s) for this ProgramInformation. */ - public final String lang; + @Nullable public final String lang; public ProgramInformation( - String title, String source, String copyright, String moreInformationURL, String lang) { + @Nullable String title, + @Nullable String source, + @Nullable String copyright, + @Nullable String moreInformationURL, + @Nullable String lang) { this.title = title; this.source = source; this.copyright = copyright; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java index c7bb4adec5..bcd783f0cb 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java @@ -83,10 +83,11 @@ public final class RangedUri { *

If {@code other} is null then the merge is considered unsuccessful, and null is returned. * * @param other The {@link RangedUri} to merge. - * @param baseUri The optional base Uri. + * @param baseUri The base Uri. * @return The merged {@link RangedUri} if the merge was successful. Null otherwise. */ - public @Nullable RangedUri attemptMerge(@Nullable RangedUri other, String baseUri) { + @Nullable + public RangedUri attemptMerge(@Nullable RangedUri other, String baseUri) { final String resolvedUri = resolveUriString(baseUri); if (other == null || !resolvedUri.equals(other.resolveUriString(baseUri))) { return null; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 0884bcc65c..80ad15cd8f 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; @@ -53,9 +54,7 @@ public abstract class Representation { * The offset of the presentation timestamps in the media stream relative to media time. */ public final long presentationTimeOffsetUs; - /** - * The in-band event streams in the representation. Never null, but may be empty. - */ + /** The in-band event streams in the representation. May be empty. */ public final List inbandEventStreams; private final RangedUri initializationUri; @@ -71,7 +70,7 @@ public abstract class Representation { */ public static Representation newInstance( long revisionId, Format format, String baseUrl, SegmentBase segmentBase) { - return newInstance(revisionId, format, baseUrl, segmentBase, null); + return newInstance(revisionId, format, baseUrl, segmentBase, /* inbandEventStreams= */ null); } /** @@ -89,8 +88,9 @@ public abstract class Representation { Format format, String baseUrl, SegmentBase segmentBase, - List inbandEventStreams) { - return newInstance(revisionId, format, baseUrl, segmentBase, inbandEventStreams, null); + @Nullable List inbandEventStreams) { + return newInstance( + revisionId, format, baseUrl, segmentBase, inbandEventStreams, /* cacheKey= */ null); } /** @@ -110,8 +110,8 @@ public abstract class Representation { Format format, String baseUrl, SegmentBase segmentBase, - List inbandEventStreams, - String cacheKey) { + @Nullable List inbandEventStreams, + @Nullable String cacheKey) { if (segmentBase instanceof SingleSegmentBase) { return new SingleSegmentRepresentation( revisionId, @@ -135,7 +135,7 @@ public abstract class Representation { Format format, String baseUrl, SegmentBase segmentBase, - List inbandEventStreams) { + @Nullable List inbandEventStreams) { this.revisionId = revisionId; this.format = format; this.baseUrl = baseUrl; @@ -151,6 +151,7 @@ public abstract class Representation { * Returns a {@link RangedUri} defining the location of the representation's initialization data, * or null if no initialization data exists. */ + @Nullable public RangedUri getInitializationUri() { return initializationUri; } @@ -159,14 +160,15 @@ public abstract class Representation { * Returns a {@link RangedUri} defining the location of the representation's segment index, or * null if the representation provides an index directly. */ + @Nullable public abstract RangedUri getIndexUri(); - /** - * Returns an index if the representation provides one directly, or null otherwise. - */ + /** Returns an index if the representation provides one directly, or null otherwise. */ + @Nullable public abstract DashSegmentIndex getIndex(); /** Returns a cache key for the representation if set, or null. */ + @Nullable public abstract String getCacheKey(); /** @@ -184,9 +186,9 @@ public abstract class Representation { */ public final long contentLength; - private final String cacheKey; - private final RangedUri indexUri; - private final SingleSegmentIndex segmentIndex; + @Nullable private final String cacheKey; + @Nullable private final RangedUri indexUri; + @Nullable private final SingleSegmentIndex segmentIndex; /** * @param revisionId Identifies the revision of the content. @@ -209,7 +211,7 @@ public abstract class Representation { long indexStart, long indexEnd, List inbandEventStreams, - String cacheKey, + @Nullable String cacheKey, long contentLength) { RangedUri rangedUri = new RangedUri(null, initializationStart, initializationEnd - initializationStart + 1); @@ -233,8 +235,8 @@ public abstract class Representation { Format format, String baseUrl, SingleSegmentBase segmentBase, - List inbandEventStreams, - String cacheKey, + @Nullable List inbandEventStreams, + @Nullable String cacheKey, long contentLength) { super(revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.uri = Uri.parse(baseUrl); @@ -248,16 +250,19 @@ public abstract class Representation { } @Override + @Nullable public RangedUri getIndexUri() { return indexUri; } @Override + @Nullable public DashSegmentIndex getIndex() { return segmentIndex; } @Override + @Nullable public String getCacheKey() { return cacheKey; } @@ -284,12 +289,13 @@ public abstract class Representation { Format format, String baseUrl, MultiSegmentBase segmentBase, - List inbandEventStreams) { + @Nullable List inbandEventStreams) { super(revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.segmentBase = segmentBase; } @Override + @Nullable public RangedUri getIndexUri() { return null; } @@ -300,6 +306,7 @@ public abstract class Representation { } @Override + @Nullable public String getCacheKey() { return null; } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index 7db44dd629..a31e0329af 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash.manifest; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; import com.google.android.exoplayer2.util.Util; @@ -25,7 +26,7 @@ import java.util.List; */ public abstract class SegmentBase { - /* package */ final RangedUri initialization; + /* package */ @Nullable final RangedUri initialization; /* package */ final long timescale; /* package */ final long presentationTimeOffset; @@ -36,7 +37,8 @@ public abstract class SegmentBase { * @param presentationTimeOffset The presentation time offset. The value in seconds is the * division of this value and {@code timescale}. */ - public SegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset) { + public SegmentBase( + @Nullable RangedUri initialization, long timescale, long presentationTimeOffset) { this.initialization = initialization; this.timescale = timescale; this.presentationTimeOffset = presentationTimeOffset; @@ -49,6 +51,7 @@ public abstract class SegmentBase { * @param representation The {@link Representation} for which initialization data is required. * @return A {@link RangedUri} defining the location of the initialization data, or null. */ + @Nullable public RangedUri getInitialization(Representation representation) { return initialization; } @@ -77,19 +80,31 @@ public abstract class SegmentBase { * @param indexStart The byte offset of the index data in the segment. * @param indexLength The length of the index data in bytes. */ - public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset, - long indexStart, long indexLength) { + public SingleSegmentBase( + @Nullable RangedUri initialization, + long timescale, + long presentationTimeOffset, + long indexStart, + long indexLength) { super(initialization, timescale, presentationTimeOffset); this.indexStart = indexStart; this.indexLength = indexLength; } public SingleSegmentBase() { - this(null, 1, 0, 0, 0); + this( + /* initialization= */ null, + /* timescale= */ 1, + /* presentationTimeOffset= */ 0, + /* indexStart= */ 0, + /* indexLength= */ 0); } + @Nullable public RangedUri getIndex() { - return indexLength <= 0 ? null : new RangedUri(null, indexStart, indexLength); + return indexLength <= 0 + ? null + : new RangedUri(/* referenceUri= */ null, indexStart, indexLength); } } @@ -101,7 +116,7 @@ public abstract class SegmentBase { /* package */ final long startNumber; /* package */ final long duration; - /* package */ final List segmentTimeline; + /* package */ @Nullable final List segmentTimeline; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -118,12 +133,12 @@ public abstract class SegmentBase { * parameter. */ public MultiSegmentBase( - RangedUri initialization, + @Nullable RangedUri initialization, long timescale, long presentationTimeOffset, long startNumber, long duration, - List segmentTimeline) { + @Nullable List segmentTimeline) { super(initialization, timescale, presentationTimeOffset); this.startNumber = startNumber; this.duration = duration; @@ -223,7 +238,7 @@ public abstract class SegmentBase { */ public static class SegmentList extends MultiSegmentBase { - /* package */ final List mediaSegments; + /* package */ @Nullable final List mediaSegments; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -246,8 +261,8 @@ public abstract class SegmentBase { long presentationTimeOffset, long startNumber, long duration, - List segmentTimeline, - List mediaSegments) { + @Nullable List segmentTimeline, + @Nullable List mediaSegments) { super(initialization, timescale, presentationTimeOffset, startNumber, duration, segmentTimeline); this.mediaSegments = mediaSegments; @@ -275,8 +290,8 @@ public abstract class SegmentBase { */ public static class SegmentTemplate extends MultiSegmentBase { - /* package */ final UrlTemplate initializationTemplate; - /* package */ final UrlTemplate mediaTemplate; + /* package */ @Nullable final UrlTemplate initializationTemplate; + /* package */ @Nullable final UrlTemplate mediaTemplate; /* package */ final long endNumber; /** @@ -287,43 +302,9 @@ public abstract class SegmentBase { * @param presentationTimeOffset The presentation time offset. The value in seconds is the * division of this value and {@code timescale}. * @param startNumber The sequence number of the first segment. - * @param duration The duration of each segment in the case of fixed duration segments. The - * value in seconds is the division of this value and {@code timescale}. If {@code - * segmentTimeline} is non-null then this parameter is ignored. - * @param segmentTimeline A segment timeline corresponding to the segments. If null, then - * segments are assumed to be of fixed duration as specified by the {@code duration} - * parameter. - * @param initializationTemplate A template defining the location of initialization data, if - * such data exists. If non-null then the {@code initialization} parameter is ignored. If - * null then {@code initialization} will be used. - * @param mediaTemplate A template defining the location of each media segment. - */ - public SegmentTemplate( - RangedUri initialization, - long timescale, - long presentationTimeOffset, - long startNumber, - long duration, - List segmentTimeline, - UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate) { - super(initialization, timescale, presentationTimeOffset, startNumber, - duration, segmentTimeline); - this.initializationTemplate = initializationTemplate; - this.mediaTemplate = mediaTemplate; - this.endNumber = C.INDEX_UNSET; - } - - /** - * @param initialization A {@link RangedUri} corresponding to initialization data, if such data - * exists. The value of this parameter is ignored if {@code initializationTemplate} is - * non-null. - * @param timescale The timescale in units per second. - * @param presentationTimeOffset The presentation time offset. The value in seconds is the - * division of this value and {@code timescale}. - * @param startNumber The sequence number of the first segment. - * @param endNumber The sequence number of the last segment specified by SupplementalProperty - * schemeIdUri="http://dashif.org/guidelines/last-segment-number" + * @param endNumber The sequence number of the last segment as specified by the + * SupplementalProperty with schemeIdUri="http://dashif.org/guidelines/last-segment-number", + * or {@link C#INDEX_UNSET}. * @param duration The duration of each segment in the case of fixed duration segments. The * value in seconds is the division of this value and {@code timescale}. If {@code * segmentTimeline} is non-null then this parameter is ignored. @@ -342,10 +323,15 @@ public abstract class SegmentBase { long startNumber, long endNumber, long duration, - List segmentTimeline, - UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate) { - super(initialization, timescale, presentationTimeOffset, startNumber, duration, + @Nullable List segmentTimeline, + @Nullable UrlTemplate initializationTemplate, + @Nullable UrlTemplate mediaTemplate) { + super( + initialization, + timescale, + presentationTimeOffset, + startNumber, + duration, segmentTimeline); this.initializationTemplate = initializationTemplate; this.mediaTemplate = mediaTemplate; @@ -353,6 +339,7 @@ public abstract class SegmentBase { } @Override + @Nullable public RangedUri getInitialization(Representation representation) { if (initializationTemplate != null) { String urlString = initializationTemplate.buildUri(representation.format.id, 0, diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/package-info.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/package-info.java new file mode 100644 index 0000000000..b7c267727c --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/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.source.dash.manifest; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/package-info.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/package-info.java new file mode 100644 index 0000000000..4eb0d8436d --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/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.source.dash.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/package-info.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/package-info.java new file mode 100644 index 0000000000..f51ea4369e --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/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.source.dash; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/dash/src/test/AndroidManifest.xml b/library/dash/src/test/AndroidManifest.xml index e20c1fbb9f..00892b77b8 100644 --- a/library/dash/src/test/AndroidManifest.xml +++ b/library/dash/src/test/AndroidManifest.xml @@ -14,4 +14,6 @@ limitations under the License. --> - + + + diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java index fa077df209..f39a493e9f 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java @@ -22,6 +22,7 @@ import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -116,6 +117,7 @@ public final class DashMediaPeriodTest { periodIndex, mock(DashChunkSource.Factory.class), mock(TransferListener.class), + DrmSessionManager.getDummyDrmSessionManager(), mock(LoadErrorHandlingPolicy.class), new EventDispatcher() .withParameters( diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index b3a6b8271b..94dae35ed5 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.offline.Downloader; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; @@ -108,7 +109,7 @@ public class DashDownloaderTest { DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -127,7 +128,7 @@ public class DashDownloaderTest { DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0)); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -146,7 +147,7 @@ public class DashDownloaderTest { DashDownloader dashDownloader = getDashDownloader(fakeDataSet, new StreamKey(0, 0, 0), new StreamKey(0, 1, 0)); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -167,7 +168,7 @@ public class DashDownloaderTest { DashDownloader dashDownloader = getDashDownloader(fakeDataSet); dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -256,7 +257,7 @@ public class DashDownloaderTest { // Expected. } dashDownloader.download(progressListener); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java index 0b7c06f813..107bf7c790 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java @@ -16,8 +16,10 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.testutil.FakeDataSource; import org.junit.Test; @@ -30,6 +32,7 @@ public final class DownloadHelperTest { @Test public void staticDownloadHelperForDash_doesNotThrow() { DownloadHelper.forDash( + ApplicationProvider.getApplicationContext(), Uri.parse("http://uri"), new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]); @@ -37,7 +40,7 @@ public final class DownloadHelperTest { Uri.parse("http://uri"), new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], - /* drmSessionManager= */ null, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS); + /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager(), + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 56fedbefd0..bc75df6acf 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread.TestRunnable; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -153,7 +154,7 @@ public class DownloadManagerDashTest { public void testHandleDownloadRequest() throws Throwable { handleDownloadRequest(fakeStreamKey1, fakeStreamKey2); blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -161,7 +162,7 @@ public class DownloadManagerDashTest { handleDownloadRequest(fakeStreamKey1); handleDownloadRequest(fakeStreamKey2); blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test @@ -175,7 +176,7 @@ public class DownloadManagerDashTest { handleDownloadRequest(fakeStreamKey1); blockUntilTasksCompleteAndThrowAnyDownloadError(); - assertCachedData(cache, fakeDataSet); + assertCachedData(cache, new RequestSet(fakeDataSet).useBoundedDataSpecFor("audio_init_data")); } @Test diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 8e9696af70..0f685c1130 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -39,10 +39,12 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java index 4fe76cdf81..fe70298dc8 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java @@ -51,7 +51,7 @@ import javax.crypto.spec.SecretKeySpec; private final byte[] encryptionKey; private final byte[] encryptionIv; - private @Nullable CipherInputStream cipherInputStream; + @Nullable private CipherInputStream cipherInputStream; /** * @param upstream The upstream {@link DataSource}. @@ -105,7 +105,8 @@ import javax.crypto.spec.SecretKeySpec; } @Override - public final @Nullable Uri getUri() { + @Nullable + public final Uri getUri() { return upstream.getUri(); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 9fde54a705..6dd4ade590 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -84,11 +84,11 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { @Override public Result createExtractor( - Extractor previousExtractor, + @Nullable Extractor previousExtractor, Uri uri, Format format, - List muxedCaptionFormats, - DrmInitData drmInitData, + @Nullable List muxedCaptionFormats, + @Nullable DrmInitData drmInitData, TimestampAdjuster timestampAdjuster, Map> responseHeaders, ExtractorInput extractorInput) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 261c9b531c..c452a29cf9 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -59,10 +59,8 @@ import java.util.Map; clear(); } - /** - * The chunk to be loaded next. - */ - public Chunk chunk; + /** The chunk to be loaded next. */ + @Nullable public Chunk chunk; /** * Indicates that the end of the stream has been reached. @@ -70,7 +68,7 @@ import java.util.Map; public boolean endOfStream; /** Indicates that the chunk source is waiting for the referred playlist to be refreshed. */ - public Uri playlistUrl; + @Nullable public Uri playlistUrl; /** * Clears the holder. @@ -138,7 +136,7 @@ import java.util.Map; HlsDataSourceFactory dataSourceFactory, @Nullable TransferListener mediaTransferListener, TimestampAdjusterProvider timestampAdjusterProvider, - List muxedCaptionFormats) { + @Nullable List muxedCaptionFormats) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.playlistUrls = playlistUrls; @@ -183,17 +181,15 @@ import java.util.Map; } /** - * Selects tracks for use. + * Sets the current track selection. * - * @param trackSelection The track selection. + * @param trackSelection The {@link TrackSelection}. */ - public void selectTracks(TrackSelection trackSelection) { + public void setTrackSelection(TrackSelection trackSelection) { this.trackSelection = trackSelection; } - /** - * Returns the current track selection. - */ + /** Returns the current {@link TrackSelection}. */ public TrackSelection getTrackSelection() { return trackSelection; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java index 103d89188f..927b79899d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.Extractor; @@ -82,11 +83,11 @@ public interface HlsExtractorFactory { * @throws IOException If an I/O error is encountered while sniffing. */ Result createExtractor( - Extractor previousExtractor, + @Nullable Extractor previousExtractor, Uri uri, Format format, - List muxedCaptionFormats, - DrmInitData drmInitData, + @Nullable List muxedCaptionFormats, + @Nullable DrmInitData drmInitData, TimestampAdjuster timestampAdjuster, Map> responseHeaders, ExtractorInput sniffingExtractorInput) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 2cfd14c79d..8053958c2b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -22,6 +22,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; @@ -52,6 +54,7 @@ import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A {@link MediaPeriod} that loads an HLS stream. @@ -62,7 +65,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final HlsExtractorFactory extractorFactory; private final HlsPlaylistTracker playlistTracker; private final HlsDataSourceFactory dataSourceFactory; - private final @Nullable TransferListener mediaTransferListener; + @Nullable private final TransferListener mediaTransferListener; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; @@ -72,7 +76,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final boolean allowChunklessPreparation; private final boolean useSessionKeys; - private @Nullable Callback callback; + @Nullable private Callback callback; private int pendingPrepareCount; private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; @@ -91,6 +95,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper * and keys. * @param mediaTransferListener The transfer listener to inform of any media data transfers. May * be null if no listener is available. + * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession + * DrmSessions} with. * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. @@ -104,6 +110,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, @Nullable TransferListener mediaTransferListener, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, Allocator allocator, @@ -114,6 +121,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; this.mediaTransferListener = mediaTransferListener; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.allocator = allocator; @@ -242,8 +250,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { // Map each selection and stream onto a child period index. int[] streamChildIndices = new int[selections.length]; int[] selectionChildIndices = new int[selections.length]; @@ -487,7 +499,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper manifestUrlIndicesPerWrapper.add(new int[] {i}); sampleStreamWrappers.add(sampleStreamWrapper); sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(new TrackGroup(subtitleRendition.format)), 0, TrackGroupArray.EMPTY); + new TrackGroup[] {new TrackGroup(subtitleRendition.format)}, + /* primaryTrackGroupIndex= */ 0); } this.sampleStreamWrappers = sampleStreamWrappers.toArray(new HlsSampleStreamWrapper[0]); @@ -645,9 +658,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper muxedTrackGroups.add(id3TrackGroup); sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(muxedTrackGroups.toArray(new TrackGroup[0])), - 0, - new TrackGroupArray(id3TrackGroup)); + muxedTrackGroups.toArray(new TrackGroup[0]), + /* primaryTrackGroupIndex= */ 0, + /* optionalTrackGroupsIndices= */ muxedTrackGroups.indexOf(id3TrackGroup)); } } @@ -703,7 +716,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper if (allowChunklessPreparation && renditionsHaveCodecs) { Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]); sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(new TrackGroup(renditionFormats)), 0, TrackGroupArray.EMPTY); + new TrackGroup[] {new TrackGroup(renditionFormats)}, /* primaryTrackGroupIndex= */ 0); } } } @@ -734,6 +747,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper allocator, positionUs, muxedAudioFormat, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index be4484aa78..f2db9541eb 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -20,6 +20,8 @@ import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BaseMediaSource; @@ -65,6 +67,7 @@ public final class HlsMediaSource extends BaseMediaSource @Nullable private List streamKeys; private HlsPlaylistTracker.Factory playlistTrackerFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean allowChunklessPreparation; private boolean useSessionKeys; @@ -93,6 +96,7 @@ public final class HlsMediaSource extends BaseMediaSource playlistParserFactory = new DefaultHlsPlaylistParserFactory(); playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY; extractorFactory = HlsExtractorFactory.DEFAULT; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } @@ -106,7 +110,7 @@ public final class HlsMediaSource extends BaseMediaSource * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setTag(Object tag) { + public Factory setTag(@Nullable Object tag) { Assertions.checkState(!isCreateCalled); this.tag = tag; return this; @@ -127,6 +131,20 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + /** * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. @@ -271,6 +289,7 @@ public final class HlsMediaSource extends BaseMediaSource hlsDataSourceFactory, extractorFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, playlistTrackerFactory.createTracker( hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory), @@ -297,19 +316,21 @@ public final class HlsMediaSource extends BaseMediaSource private final Uri manifestUri; private final HlsDataSourceFactory dataSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final boolean allowChunklessPreparation; private final boolean useSessionKeys; private final HlsPlaylistTracker playlistTracker; - private final @Nullable Object tag; + @Nullable private final Object tag; - private @Nullable TransferListener mediaTransferListener; + @Nullable private TransferListener mediaTransferListener; private HlsMediaSource( Uri manifestUri, HlsDataSourceFactory dataSourceFactory, HlsExtractorFactory extractorFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, HlsPlaylistTracker playlistTracker, boolean allowChunklessPreparation, @@ -319,6 +340,7 @@ public final class HlsMediaSource extends BaseMediaSource this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.playlistTracker = playlistTracker; this.allowChunklessPreparation = allowChunklessPreparation; @@ -333,7 +355,7 @@ public final class HlsMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this); @@ -352,6 +374,7 @@ public final class HlsMediaSource extends BaseMediaSource playlistTracker, dataSourceFactory, mediaTransferListener, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher, allocator, @@ -366,7 +389,7 @@ public final class HlsMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { playlistTracker.stop(); } @@ -383,6 +406,7 @@ public final class HlsMediaSource extends BaseMediaSource ? windowStartTimeMs : C.TIME_UNSET; long windowDefaultStartPositionUs = playlist.startOffsetUs; + HlsManifest manifest = new HlsManifest(playlistTracker.getMasterPlaylist(), playlist); if (playlistTracker.isLive()) { long offsetFromInitialStartTimeUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); @@ -403,6 +427,7 @@ public final class HlsMediaSource extends BaseMediaSource windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ !playlist.hasEndTag, + manifest, tag); } else /* not live */ { if (windowDefaultStartPositionUs == C.TIME_UNSET) { @@ -418,9 +443,10 @@ public final class HlsMediaSource extends BaseMediaSource windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ false, + manifest, tag); } - refreshSourceInfo(timeline, new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); + refreshSourceInfo(timeline); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index cf879e91c6..c820038b80 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -62,8 +62,11 @@ import java.io.IOException; if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) { throw new SampleQueueMappingException( sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); + } else if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { + sampleStreamWrapper.maybeThrowError(); + } else if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL) { + sampleStreamWrapper.maybeThrowError(sampleQueueIndex); } - sampleStreamWrapper.maybeThrowError(); } @Override diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 65039b9364..ff725ec6f7 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -23,12 +23,15 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.PrivFrame; +import com.google.android.exoplayer2.source.DecryptableSampleQueueReader; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; @@ -51,8 +54,10 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides @@ -92,6 +97,7 @@ import java.util.Map; private final HlsChunkSource chunkSource; private final Allocator allocator; private final Format muxedAudioFormat; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final Loader loader; private final EventDispatcher eventDispatcher; @@ -105,6 +111,7 @@ import java.util.Map; private final Map overridingDrmInitData; private SampleQueue[] sampleQueues; + private DecryptableSampleQueueReader[] sampleQueueReaders; private int[] sampleQueueTrackIds; private boolean audioSampleQueueMappingDone; private int audioSampleQueueIndex; @@ -122,7 +129,7 @@ import java.util.Map; // Tracks are complicated in HLS. See documentation of buildTracks for details. // Indexed by track (as exposed by this source). private TrackGroupArray trackGroups; - private TrackGroupArray optionalTrackGroups; + private Set optionalTrackGroups; // Indexed by track group. private int[] trackGroupToSampleQueueIndex; private int primaryTrackGroupIndex; @@ -152,6 +159,8 @@ import java.util.Map; * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. + * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession + * DrmSessions} with. * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. */ @@ -163,6 +172,7 @@ import java.util.Map; Allocator allocator, long positionUs, Format muxedAudioFormat, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher) { this.trackType = trackType; @@ -171,6 +181,7 @@ import java.util.Map; this.overridingDrmInitData = overridingDrmInitData; this.allocator = allocator; this.muxedAudioFormat = muxedAudioFormat; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; loader = new Loader("Loader:HlsSampleStreamWrapper"); @@ -179,6 +190,7 @@ import java.util.Map; audioSampleQueueIndex = C.INDEX_UNSET; videoSampleQueueIndex = C.INDEX_UNSET; sampleQueues = new SampleQueue[0]; + sampleQueueReaders = new DecryptableSampleQueueReader[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new ArrayList<>(); @@ -200,18 +212,20 @@ import java.util.Map; /** * Prepares the sample stream wrapper with master playlist information. * - * @param trackGroups The {@link TrackGroupArray} to expose. + * @param trackGroups The {@link TrackGroup TrackGroups} to expose through {@link + * #getTrackGroups()}. * @param primaryTrackGroupIndex The index of the adaptive track group. - * @param optionalTrackGroups A subset of {@code trackGroups} that should not trigger a failure if - * not found in the media playlist's segments. + * @param optionalTrackGroupsIndices The indices of any {@code trackGroups} that should not + * trigger a failure if not found in the media playlist's segments. */ public void prepareWithMasterPlaylistInfo( - TrackGroupArray trackGroups, - int primaryTrackGroupIndex, - TrackGroupArray optionalTrackGroups) { + TrackGroup[] trackGroups, int primaryTrackGroupIndex, int... optionalTrackGroupsIndices) { prepared = true; - this.trackGroups = trackGroups; - this.optionalTrackGroups = optionalTrackGroups; + this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); + optionalTrackGroups = new HashSet<>(); + for (int optionalTrackGroupIndex : optionalTrackGroupsIndices) { + optionalTrackGroups.add(this.trackGroups.get(optionalTrackGroupIndex)); + } this.primaryTrackGroupIndex = primaryTrackGroupIndex; handler.post(callback::onPrepared); } @@ -231,9 +245,9 @@ import java.util.Map; public int bindSampleQueueToSampleStream(int trackGroupIndex) { int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; if (sampleQueueIndex == C.INDEX_UNSET) { - return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET - ? SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL - : SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL; + return optionalTrackGroups.contains(trackGroups.get(trackGroupIndex)) + ? SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL + : SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; } if (sampleQueuesEnabledStates[sampleQueueIndex]) { // This sample queue is already bound to a different sample stream. @@ -292,14 +306,17 @@ import java.util.Map; TrackSelection primaryTrackSelection = oldPrimaryTrackSelection; // Select new tracks. for (int i = 0; i < selections.length; i++) { - if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + if (selection == null) { + continue; + } + int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); + if (trackGroupIndex == primaryTrackGroupIndex) { + primaryTrackSelection = selection; + chunkSource.setTrackSelection(selection); + } + if (streams[i] == null) { enabledTrackGroupCount++; - TrackSelection selection = selections[i]; - int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); - if (trackGroupIndex == primaryTrackGroupIndex) { - primaryTrackSelection = selection; - chunkSource.selectTracks(selection); - } streams[i] = new HlsSampleStream(this, trackGroupIndex); streamResetFlags[i] = true; if (trackGroupToSampleQueueIndex != null) { @@ -322,6 +339,7 @@ import java.util.Map; if (enabledTrackGroupCount == 0) { chunkSource.reset(); downstreamTrackFormat = null; + pendingResetUpstreamFormats = true; mediaChunks.clear(); if (loader.isLoading()) { if (sampleQueuesBuilt) { @@ -433,6 +451,9 @@ import java.util.Map; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.discardToEnd(); } + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } } loader.release(this); handler.removeCallbacksAndMessages(null); @@ -443,6 +464,9 @@ import java.util.Map; @Override public void onLoaderReleased() { resetSampleQueues(); + for (DecryptableSampleQueueReader reader : sampleQueueReaders) { + reader.release(); + } } public void setIsTimestampMaster(boolean isTimestampMaster) { @@ -456,7 +480,12 @@ import java.util.Map; // SampleStream implementation. public boolean isReady(int sampleQueueIndex) { - return loadingFinished || (!isPendingReset() && sampleQueues[sampleQueueIndex].hasNextSample()); + return !isPendingReset() && sampleQueueReaders[sampleQueueIndex].isReady(loadingFinished); + } + + public void maybeThrowError(int sampleQueueIndex) throws IOException { + maybeThrowError(); + sampleQueueReaders[sampleQueueIndex].maybeThrowError(); } public void maybeThrowError() throws IOException { @@ -489,7 +518,7 @@ import java.util.Map; } int result = - sampleQueues[sampleQueueIndex].read( + sampleQueueReaders[sampleQueueIndex].read( formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); if (result == C.RESULT_FORMAT_READ) { Format format = formatHolder.format; @@ -506,12 +535,6 @@ import java.util.Map; : upstreamTrackFormat; format = format.copyWithManifestFormatInfo(trackFormat); } - if (format.drmInitData != null) { - DrmInitData drmInitData = overridingDrmInitData.get(format.drmInitData.schemeType); - if (drmInitData != null) { - format = format.copyWithDrmInitData(drmInitData); - } - } formatHolder.format = format; } return result; @@ -818,7 +841,7 @@ import java.util.Map; return createDummyTrackOutput(id, type); } } - SampleQueue trackOutput = new PrivTimestampStrippingSampleQueue(allocator); + SampleQueue trackOutput = new FormatAdjustingSampleQueue(allocator, overridingDrmInitData); trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.sourceId(chunkUid); trackOutput.setUpstreamFormatChangeListener(this); @@ -826,6 +849,9 @@ import java.util.Map; sampleQueueTrackIds[trackCount] = id; sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1); sampleQueues[trackCount] = trackOutput; + sampleQueueReaders = Arrays.copyOf(sampleQueueReaders, trackCount + 1); + sampleQueueReaders[trackCount] = + new DecryptableSampleQueueReader(sampleQueues[trackCount], drmSessionManager); sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1); sampleQueueIsAudioVideoFlags[trackCount] = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; @@ -1038,9 +1064,27 @@ import java.util.Map; trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false)); } } - this.trackGroups = new TrackGroupArray(trackGroups); + this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); Assertions.checkState(optionalTrackGroups == null); - optionalTrackGroups = TrackGroupArray.EMPTY; + optionalTrackGroups = Collections.emptySet(); + } + + private TrackGroupArray createTrackGroupArrayWithDrmInfo(TrackGroup[] trackGroups) { + for (int i = 0; i < trackGroups.length; i++) { + TrackGroup trackGroup = trackGroups[i]; + Format[] exposedFormats = new Format[trackGroup.length]; + for (int j = 0; j < trackGroup.length; j++) { + Format format = trackGroup.getFormat(j); + if (format.drmInitData != null) { + format = + format.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(format.drmInitData)); + } + exposedFormats[j] = format; + } + trackGroups[i] = new TrackGroup(exposedFormats); + } + return new TrackGroupArray(trackGroups); } private HlsMediaChunk getLastMediaChunk() { @@ -1160,15 +1204,26 @@ import java.util.Map; return new DummyTrackOutput(); } - private static final class PrivTimestampStrippingSampleQueue extends SampleQueue { + private static final class FormatAdjustingSampleQueue extends SampleQueue { - public PrivTimestampStrippingSampleQueue(Allocator allocator) { + private final Map overridingDrmInitData; + + public FormatAdjustingSampleQueue( + Allocator allocator, Map overridingDrmInitData) { super(allocator); + this.overridingDrmInitData = overridingDrmInitData; } @Override public void format(Format format) { - super.format(format.copyWithMetadata(getAdjustedMetadata(format.metadata))); + DrmInitData drmInitData = format.drmInitData; + if (drmInitData != null) { + DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType); + if (overridingDrmInitData != null) { + drmInitData = overridingDrmInitData; + } + } + super.format(format.copyWithAdjustments(drmInitData, getAdjustedMetadata(format.metadata))); } /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java index 14268313eb..2ba3b45ca0 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java @@ -184,6 +184,11 @@ public final class HlsTrackMetadataEntry implements Metadata.Entry { this.variantInfos = Collections.unmodifiableList(variantInfos); } + @Override + public String toString() { + return "HlsTrackMetadataEntry" + (groupId != null ? (" [" + groupId + ", " + name + "]") : ""); + } + @Override public boolean equals(@Nullable Object other) { if (this == other) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java index 665f2e0570..a89e907a37 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.hls; +import androidx.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -49,7 +50,7 @@ public final class WebvttExtractor implements Extractor { private static final int HEADER_MIN_LENGTH = 6 /* "WEBVTT" */; private static final int HEADER_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH; - private final String language; + @Nullable private final String language; private final TimestampAdjuster timestampAdjuster; private final ParsableByteArray sampleDataWrapper; @@ -58,7 +59,7 @@ public final class WebvttExtractor implements Extractor { private byte[] sampleData; private int sampleSize; - public WebvttExtractor(String language, TimestampAdjuster timestampAdjuster) { + public WebvttExtractor(@Nullable String language, TimestampAdjuster timestampAdjuster) { this.language = language; this.timestampAdjuster = timestampAdjuster; this.sampleDataWrapper = new ParsableByteArray(); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/package-info.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/package-info.java new file mode 100644 index 0000000000..2527553824 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/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.source.hls.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/package-info.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/package-info.java new file mode 100644 index 0000000000..55f15f5e7a --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/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.source.hls; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java index 0064338ca8..e7a072839e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -166,11 +166,13 @@ public final class DefaultHlsPlaylistTracker } @Override - public @Nullable HlsMasterPlaylist getMasterPlaylist() { + @Nullable + public HlsMasterPlaylist getMasterPlaylist() { return masterPlaylist; } @Override + @Nullable public HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback) { HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); if (snapshot != null && isForPlayback) { @@ -447,7 +449,7 @@ public final class DefaultHlsPlaylistTracker private final Loader mediaPlaylistLoader; private final ParsingLoadable mediaPlaylistLoadable; - private HlsMediaPlaylist playlistSnapshot; + @Nullable private HlsMediaPlaylist playlistSnapshot; private long lastSnapshotLoadMs; private long lastSnapshotChangeMs; private long earliestNextLoadTimeMs; @@ -466,6 +468,7 @@ public final class DefaultHlsPlaylistTracker mediaPlaylistParser); } + @Nullable public HlsMediaPlaylist getPlaylistSnapshot() { return playlistSnapshot; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 0e86df8c2f..1660324a34 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -174,7 +174,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { * The format of the audio muxed in the variants. May be null if the playlist does not declare any * muxed audio. */ - public final Format muxedAudioFormat; + @Nullable public final Format muxedAudioFormat; /** * The format of the closed captions declared by the playlist. May be empty if the playlist * explicitly declares no captions are available, or null if the playlist does not declare any @@ -208,7 +208,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { List audios, List subtitles, List closedCaptions, - Format muxedAudioFormat, + @Nullable Format muxedAudioFormat, List muxedCaptionFormats, boolean hasIndependentSegments, Map variableDefinitions, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 4411c9865e..58f500cf94 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.hls.playlist; import androidx.annotation.IntDef; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData; @@ -95,8 +94,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { String uri, long byterangeOffset, long byterangeLength, - String fullSegmentEncryptionKeyUri, - String encryptionIV) { + @Nullable String fullSegmentEncryptionKeyUri, + @Nullable String encryptionIV) { this( uri, /* initializationSegment= */ null, @@ -154,7 +153,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } @Override - public int compareTo(@NonNull Long relativeStartTimeUs) { + public int compareTo(Long relativeStartTimeUs) { return this.relativeStartTimeUs > relativeStartTimeUs ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/package-info.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/package-info.java new file mode 100644 index 0000000000..61f9d77e72 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/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.source.hls.playlist; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/hls/src/test/AndroidManifest.xml b/library/hls/src/test/AndroidManifest.xml index 326ff48b16..356a814026 100644 --- a/library/hls/src/test/AndroidManifest.xml +++ b/library/hls/src/test/AndroidManifest.xml @@ -14,4 +14,6 @@ limitations under the License. --> - + + + diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java index dc9c0e0644..93b8be3346 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java @@ -15,13 +15,14 @@ */ package com.google.android.exoplayer2.source.hls; -import static org.mockito.Matchers.anyInt; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -81,6 +82,7 @@ public final class HlsMediaPeriodTest { mockPlaylistTracker, mockDataSourceFactory, mock(TransferListener.class), + mock(DrmSessionManager.class), mock(LoadErrorHandlingPolicy.class), new EventDispatcher() .withParameters( diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java index c7a8034ee7..3c81074c25 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.hls.offline; import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.offline.DownloadHelper; @@ -30,6 +31,7 @@ public final class DownloadHelperTest { @Test public void staticDownloadHelperForHls_doesNotThrow() { DownloadHelper.forHls( + ApplicationProvider.getApplicationContext(), Uri.parse("http://uri"), new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]); @@ -38,6 +40,6 @@ public final class DownloadHelperTest { new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], /* drmSessionManager= */ null, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS); + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); } } diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java index 7d77a78316..d06d047f66 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderFactory; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; import com.google.android.exoplayer2.upstream.DummyDataSource; @@ -129,12 +130,13 @@ public class HlsDownloaderTest { assertCachedData( cache, - fakeDataSet, - MASTER_PLAYLIST_URI, - MEDIA_PLAYLIST_1_URI, - MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); + new RequestSet(fakeDataSet) + .subset( + MASTER_PLAYLIST_URI, + MEDIA_PLAYLIST_1_URI, + MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts")); } @Test @@ -186,11 +188,12 @@ public class HlsDownloaderTest { assertCachedData( cache, - fakeDataSet, - MEDIA_PLAYLIST_1_URI, - MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", - MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); + new RequestSet(fakeDataSet) + .subset( + MEDIA_PLAYLIST_1_URI, + MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts")); } @Test diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 095739271e..254a2b2bd1 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -263,7 +263,7 @@ public class HlsMasterPlaylistParserTest { Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); assertThat(closedCaptionFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA708); assertThat(closedCaptionFormat.accessibilityChannel).isEqualTo(4); - assertThat(closedCaptionFormat.language).isEqualTo("spa"); + assertThat(closedCaptionFormat.language).isEqualTo("es"); } @Test diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index a2e81fb304..b16157f49b 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -41,8 +41,10 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.2' - testImplementation project(modulePrefix + 'testutils-robolectric') + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion + implementation 'androidx.annotation:annotation:1.1.0' + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index 59e18195e2..22dfb04f13 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -74,10 +74,10 @@ public class DefaultSsChunkSource implements SsChunkSource { private final LoaderErrorThrower manifestLoaderErrorThrower; private final int streamElementIndex; - private final TrackSelection trackSelection; private final ChunkExtractorWrapper[] extractorWrappers; private final DataSource dataSource; + private TrackSelection trackSelection; private SsManifest manifest; private int currentManifestChunkOffset; @@ -155,6 +155,11 @@ public class DefaultSsChunkSource implements SsChunkSource { manifest = newManifest; } + @Override + public void updateTrackSelection(TrackSelection trackSelection) { + this.trackSelection = trackSelection; + } + // ChunkSource implementation. @Override diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java index b763a484b8..111393140e 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java @@ -55,4 +55,11 @@ public interface SsChunkSource extends ChunkSource { * @param newManifest The new manifest. */ void updateManifest(SsManifest newManifest); + + /** + * Updates the track selection. + * + * @param trackSelection The new track selection instance. Must be equivalent to the previous one. + */ + void updateTrackSelection(TrackSelection trackSelection); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 135ee4a58e..b3d950959a 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -17,7 +17,9 @@ package com.google.android.exoplayer2.source.smoothstreaming; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -36,21 +38,23 @@ import com.google.android.exoplayer2.upstream.TransferListener; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** A SmoothStreaming {@link MediaPeriod}. */ /* package */ final class SsMediaPeriod implements MediaPeriod, SequenceableLoader.Callback> { private final SsChunkSource.Factory chunkSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; private final LoaderErrorThrower manifestLoaderErrorThrower; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; private final TrackGroupArray trackGroups; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private @Nullable Callback callback; + @Nullable private Callback callback; private SsManifest manifest; private ChunkSampleStream[] sampleStreams; private SequenceableLoader compositeSequenceableLoader; @@ -61,6 +65,7 @@ import java.util.List; SsChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, LoaderErrorThrower manifestLoaderErrorThrower, @@ -69,11 +74,12 @@ import java.util.List; this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; - trackGroups = buildTrackGroups(manifest); + trackGroups = buildTrackGroups(manifest, drmSessionManager); sampleStreams = newSampleStreamArray(0); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); @@ -115,8 +121,12 @@ import java.util.List; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { ArrayList> sampleStreamsList = new ArrayList<>(); for (int i = 0; i < selections.length; i++) { if (streams[i] != null) { @@ -126,6 +136,7 @@ import java.util.List; stream.release(); streams[i] = null; } else { + stream.getChunkSource().updateTrackSelection(selections[i]); sampleStreamsList.add(stream); } } @@ -237,14 +248,26 @@ import java.util.List; this, allocator, positionUs, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher); } - private static TrackGroupArray buildTrackGroups(SsManifest manifest) { + private static TrackGroupArray buildTrackGroups( + SsManifest manifest, DrmSessionManager drmSessionManager) { TrackGroup[] trackGroups = new TrackGroup[manifest.streamElements.length]; for (int i = 0; i < manifest.streamElements.length; i++) { - trackGroups[i] = new TrackGroup(manifest.streamElements[i].formats); + Format[] manifestFormats = manifest.streamElements[i].formats; + Format[] exposedFormats = new Format[manifestFormats.length]; + for (int j = 0; j < manifestFormats.length; j++) { + Format manifestFormat = manifestFormats[j]; + exposedFormats[j] = + manifestFormat.drmInitData != null + ? manifestFormat.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(manifestFormat.drmInitData)) + : manifestFormat; + } + trackGroups[i] = new TrackGroup(exposedFormats); } return new TrackGroupArray(trackGroups); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 7b9f3e3c4f..9ddc7aa0f0 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -22,6 +22,8 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.FilteringManifestParser; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BaseMediaSource; @@ -69,6 +71,7 @@ public final class SsMediaSource extends BaseMediaSource @Nullable private ParsingLoadable.Parser manifestParser; @Nullable private List streamKeys; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; private boolean isCreateCalled; @@ -98,6 +101,7 @@ public final class SsMediaSource extends BaseMediaSource @Nullable DataSource.Factory manifestDataSourceFactory) { this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); @@ -111,12 +115,26 @@ public final class SsMediaSource extends BaseMediaSource * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setTag(Object tag) { + public Factory setTag(@Nullable Object tag) { Assertions.checkState(!isCreateCalled); this.tag = tag; return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = drmSessionManager; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. See {@link * #setLoadErrorHandlingPolicy} for the default value. @@ -220,6 +238,7 @@ public final class SsMediaSource extends BaseMediaSource /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, tag); @@ -279,6 +298,7 @@ public final class SsMediaSource extends BaseMediaSource manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, tag); @@ -318,17 +338,18 @@ public final class SsMediaSource extends BaseMediaSource private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long livePresentationDelayMs; private final EventDispatcher manifestEventDispatcher; private final ParsingLoadable.Parser manifestParser; private final ArrayList mediaPeriods; - private final @Nullable Object tag; + @Nullable private final Object tag; private DataSource manifestDataSource; private Loader manifestLoader; private LoaderErrorThrower manifestLoaderErrorThrower; - private @Nullable TransferListener mediaTransferListener; + @Nullable private TransferListener mediaTransferListener; private long manifestLoadStartTimestamp; private SsManifest manifest; @@ -349,8 +370,8 @@ public final class SsMediaSource extends BaseMediaSource public SsMediaSource( SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, chunkSourceFactory, @@ -374,8 +395,8 @@ public final class SsMediaSource extends BaseMediaSource SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, /* manifestUri= */ null, @@ -383,6 +404,7 @@ public final class SsMediaSource extends BaseMediaSource /* manifestParser= */ null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), DEFAULT_LIVE_PRESENTATION_DELAY_MS, /* tag= */ null); @@ -409,8 +431,8 @@ public final class SsMediaSource extends BaseMediaSource Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifestUri, manifestDataSourceFactory, @@ -444,8 +466,8 @@ public final class SsMediaSource extends BaseMediaSource SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new SsManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -474,8 +496,8 @@ public final class SsMediaSource extends BaseMediaSource SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( /* manifest= */ null, manifestUri, @@ -483,6 +505,7 @@ public final class SsMediaSource extends BaseMediaSource manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), livePresentationDelayMs, /* tag= */ null); @@ -492,12 +515,13 @@ public final class SsMediaSource extends BaseMediaSource } private SsMediaSource( - SsManifest manifest, - Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, - ParsingLoadable.Parser manifestParser, + @Nullable SsManifest manifest, + @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + @Nullable ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, @Nullable Object tag) { @@ -508,6 +532,7 @@ public final class SsMediaSource extends BaseMediaSource this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.livePresentationDelayMs = livePresentationDelayMs; this.manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); @@ -525,7 +550,7 @@ public final class SsMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; if (sideloadedManifest) { manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy(); @@ -553,6 +578,7 @@ public final class SsMediaSource extends BaseMediaSource chunkSourceFactory, mediaTransferListener, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher, manifestLoaderErrorThrower, @@ -568,7 +594,7 @@ public final class SsMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { manifest = sideloadedManifest ? manifest : null; manifestDataSource = null; manifestLoadStartTimestamp = 0; @@ -669,6 +695,7 @@ public final class SsMediaSource extends BaseMediaSource /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ true, manifest.isLive, + manifest, tag); } else if (manifest.isLive) { if (manifest.dvrWindowLengthUs != C.TIME_UNSET && manifest.dvrWindowLengthUs > 0) { @@ -690,6 +717,7 @@ public final class SsMediaSource extends BaseMediaSource defaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ true, + manifest, tag); } else { long durationUs = manifest.durationUs != C.TIME_UNSET ? manifest.durationUs @@ -702,9 +730,10 @@ public final class SsMediaSource extends BaseMediaSource /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ true, /* isDynamic= */ false, + manifest, tag); } - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); } private void scheduleManifestRefresh() { diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index cfb772a86b..b91bfc8f67 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.manifest; import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; @@ -69,7 +70,7 @@ public class SsManifest implements FilterableManifest { public final int maxHeight; public final int displayWidth; public final int displayHeight; - public final String language; + @Nullable public final String language; public final Format[] formats; public final int chunkCount; @@ -80,9 +81,20 @@ public class SsManifest implements FilterableManifest { private final long[] chunkStartTimesUs; private final long lastChunkDurationUs; - public StreamElement(String baseUri, String chunkTemplate, int type, String subType, - long timescale, String name, int maxWidth, int maxHeight, int displayWidth, - int displayHeight, String language, Format[] formats, List chunkStartTimes, + public StreamElement( + String baseUri, + String chunkTemplate, + int type, + String subType, + long timescale, + String name, + int maxWidth, + int maxHeight, + int displayWidth, + int displayHeight, + @Nullable String language, + Format[] formats, + List chunkStartTimes, long lastChunkDuration) { this( baseUri, @@ -102,10 +114,22 @@ public class SsManifest implements FilterableManifest { Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale)); } - private StreamElement(String baseUri, String chunkTemplate, int type, String subType, - long timescale, String name, int maxWidth, int maxHeight, int displayWidth, - int displayHeight, String language, Format[] formats, List chunkStartTimes, - long[] chunkStartTimesUs, long lastChunkDurationUs) { + private StreamElement( + String baseUri, + String chunkTemplate, + int type, + String subType, + long timescale, + String name, + int maxWidth, + int maxHeight, + int displayWidth, + int displayHeight, + @Nullable String language, + Format[] formats, + List chunkStartTimes, + long[] chunkStartTimesUs, + long lastChunkDurationUs) { this.baseUri = baseUri; this.chunkTemplate = chunkTemplate; this.type = type; @@ -208,7 +232,7 @@ public class SsManifest implements FilterableManifest { public final boolean isLive; /** Content protection information, or null if the content is not protected. */ - public final ProtectionElement protectionElement; + @Nullable public final ProtectionElement protectionElement; /** The contained stream elements. */ public final StreamElement[] streamElements; @@ -249,7 +273,7 @@ public class SsManifest implements FilterableManifest { long dvrWindowLength, int lookAheadCount, boolean isLive, - ProtectionElement protectionElement, + @Nullable ProtectionElement protectionElement, StreamElement[] streamElements) { this( majorVersion, @@ -273,7 +297,7 @@ public class SsManifest implements FilterableManifest { long dvrWindowLengthUs, int lookAheadCount, boolean isLive, - ProtectionElement protectionElement, + @Nullable ProtectionElement protectionElement, StreamElement[] streamElements) { this.majorVersion = majorVersion; this.minorVersion = minorVersion; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java index 66731660f5..03e9e91e22 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.manifest; import android.net.Uri; +import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Base64; import android.util.Pair; @@ -40,6 +41,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.UUID; +import org.checkerframework.checker.nullness.compatqual.NullableType; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; @@ -94,10 +96,10 @@ public class SsManifestParser implements ParsingLoadable.Parser { private final String baseUri; private final String tag; - private final ElementParser parent; - private final List> normalizedAttributes; + @Nullable private final ElementParser parent; + private final List> normalizedAttributes; - public ElementParser(ElementParser parent, String baseUri, String tag) { + public ElementParser(@Nullable ElementParser parent, String baseUri, String tag) { this.parent = parent; this.baseUri = baseUri; this.tag = tag; @@ -174,24 +176,25 @@ public class SsManifestParser implements ParsingLoadable.Parser { * Stash an attribute that may be normalized at this level. In other words, an attribute that * may have been pulled up from the child elements because its value was the same in all * children. - *

- * Stashing an attribute allows child element parsers to retrieve the values of normalized + * + *

Stashing an attribute allows child element parsers to retrieve the values of normalized * attributes using {@link #getNormalizedAttribute(String)}. * * @param key The name of the attribute. * @param value The value of the attribute. */ - protected final void putNormalizedAttribute(String key, Object value) { + protected final void putNormalizedAttribute(String key, @Nullable Object value) { normalizedAttributes.add(Pair.create(key, value)); } /** - * Attempt to retrieve a stashed normalized attribute. If there is no stashed attribute with - * the provided name, the parent element parser will be queried, and so on up the chain. + * Attempt to retrieve a stashed normalized attribute. If there is no stashed attribute with the + * provided name, the parent element parser will be queried, and so on up the chain. * * @param key The name of the attribute. * @return The stashed value, or null if the attribute was not be found. */ + @Nullable protected final Object getNormalizedAttribute(String key) { for (int i = 0; i < normalizedAttributes.size(); i++) { Pair pair = normalizedAttributes.get(i); @@ -340,7 +343,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { private long dvrWindowLength; private int lookAheadCount; private boolean isLive; - private ProtectionElement protectionElement; + @Nullable private ProtectionElement protectionElement; public SmoothStreamingMediaParser(ElementParser parent, String baseUri) { super(parent, baseUri, TAG); @@ -586,6 +589,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { } else { subType = parser.getAttributeValue(null, KEY_SUB_TYPE); } + putNormalizedAttribute(KEY_SUB_TYPE, subType); name = parser.getAttributeValue(null, KEY_NAME); url = parseRequiredString(parser, KEY_URL); maxWidth = parseInt(parser, KEY_MAX_WIDTH, Format.NO_VALUE); @@ -645,6 +649,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { private static final String KEY_CHANNELS = "Channels"; private static final String KEY_FOUR_CC = "FourCC"; private static final String KEY_TYPE = "Type"; + private static final String KEY_SUB_TYPE = "Subtype"; private static final String KEY_LANGUAGE = "Language"; private static final String KEY_NAME = "Name"; private static final String KEY_MAX_WIDTH = "MaxWidth"; @@ -709,6 +714,18 @@ public class SsManifestParser implements ParsingLoadable.Parser { /* roleFlags= */ 0, language); } else if (type == C.TRACK_TYPE_TEXT) { + String subType = (String) getNormalizedAttribute(KEY_SUB_TYPE); + @C.RoleFlags int roleFlags = 0; + switch (subType) { + case "CAPT": + roleFlags = C.ROLE_FLAG_CAPTION; + break; + case "DESC": + roleFlags = C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND; + break; + default: + break; + } String language = (String) getNormalizedAttribute(KEY_LANGUAGE); format = Format.createTextContainerFormat( @@ -719,7 +736,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { /* codecs= */ null, bitrate, /* selectionFlags= */ 0, - /* roleFlags= */ 0, + roleFlags, language); } else { format = diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/package-info.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/package-info.java new file mode 100644 index 0000000000..b594ddc2bc --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/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.source.smoothstreaming.manifest; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/package-info.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/package-info.java new file mode 100644 index 0000000000..f7c74f1a1e --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/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.source.smoothstreaming.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/package-info.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/package-info.java new file mode 100644 index 0000000000..23e85850c6 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/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.source.smoothstreaming; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/smoothstreaming/src/test/AndroidManifest.xml b/library/smoothstreaming/src/test/AndroidManifest.xml index 712169a7d0..df5643a1b2 100644 --- a/library/smoothstreaming/src/test/AndroidManifest.xml +++ b/library/smoothstreaming/src/test/AndroidManifest.xml @@ -14,4 +14,6 @@ limitations under the License. --> - + + + diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java index 787659fffe..b9c63f843d 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.mock; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -66,6 +67,7 @@ public class SsMediaPeriodTest { mock(SsChunkSource.Factory.class), mock(TransferListener.class), mock(CompositeSequenceableLoaderFactory.class), + mock(DrmSessionManager.class), mock(LoadErrorHandlingPolicy.class), new EventDispatcher() .withParameters( diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java index 4da08f7631..a103f89cec 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.offline; import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.offline.DownloadHelper; @@ -30,6 +31,7 @@ public final class DownloadHelperTest { @Test public void staticDownloadHelperForSmoothStreaming_doesNotThrow() { DownloadHelper.forSmoothStreaming( + ApplicationProvider.getApplicationContext(), Uri.parse("http://uri"), new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]); @@ -38,6 +40,6 @@ public final class DownloadHelperTest { new FakeDataSource.Factory(), (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], /* drmSessionManager= */ null, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS); + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); } } diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 6384bf920f..5b3123e302 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -41,9 +41,10 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.media:media:1.0.1' - 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/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index d4a37ea4ef..268219b6d5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ui; import android.content.Context; import android.content.res.TypedArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import android.util.AttributeSet; import android.widget.FrameLayout; import java.lang.annotation.Documented; @@ -97,16 +98,16 @@ public final class AspectRatioFrameLayout extends FrameLayout { private final AspectRatioUpdateDispatcher aspectRatioUpdateDispatcher; - private AspectRatioListener aspectRatioListener; + @Nullable private AspectRatioListener aspectRatioListener; private float videoAspectRatio; - private @ResizeMode int resizeMode; + @ResizeMode private int resizeMode; public AspectRatioFrameLayout(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public AspectRatioFrameLayout(Context context, AttributeSet attrs) { + public AspectRatioFrameLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); resizeMode = RESIZE_MODE_FIT; if (attrs != null) { @@ -136,9 +137,10 @@ public final class AspectRatioFrameLayout extends FrameLayout { /** * Sets the {@link AspectRatioListener}. * - * @param listener The listener to be notified about aspect ratios changes. + * @param listener The listener to be notified about aspect ratios changes, or null to clear a + * listener that was previously set. */ - public void setAspectRatioListener(AspectRatioListener listener) { + public void setAspectRatioListener(@Nullable AspectRatioListener listener) { this.aspectRatioListener = listener; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 328b5d6a49..69a2cf96be 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -190,7 +190,7 @@ public class DefaultTimeBar extends View implements TimeBar { private final Paint adMarkerPaint; private final Paint playedAdMarkerPaint; private final Paint scrubberPaint; - private final @Nullable Drawable scrubberDrawable; + @Nullable private final Drawable scrubberDrawable; private final int barHeight; private final int touchTargetHeight; private final int adMarkerWidth; @@ -217,14 +217,29 @@ public class DefaultTimeBar extends View implements TimeBar { private long position; private long bufferedPosition; private int adGroupCount; - private @Nullable long[] adGroupTimesMs; - private @Nullable boolean[] playedAdGroups; + @Nullable private long[] adGroupTimesMs; + @Nullable private boolean[] playedAdGroups; + + public DefaultTimeBar(Context context) { + this(context, null); + } + + public DefaultTimeBar(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public DefaultTimeBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, attrs); + } - /** Creates a new time bar. */ // Suppress warnings due to usage of View methods in the constructor. @SuppressWarnings("nullness:method.invocation.invalid") - public DefaultTimeBar(Context context, AttributeSet attrs) { - super(context, attrs); + public DefaultTimeBar( + Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + @Nullable AttributeSet timebarAttrs) { + super(context, attrs, defStyleAttr); seekBounds = new Rect(); progressBar = new Rect(); bufferedBar = new Rect(); @@ -251,9 +266,9 @@ public class DefaultTimeBar extends View implements TimeBar { int defaultScrubberEnabledSize = dpToPx(density, DEFAULT_SCRUBBER_ENABLED_SIZE_DP); int defaultScrubberDisabledSize = dpToPx(density, DEFAULT_SCRUBBER_DISABLED_SIZE_DP); int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP); - if (attrs != null) { - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DefaultTimeBar, 0, - 0); + if (timebarAttrs != null) { + TypedArray a = + context.getTheme().obtainStyledAttributes(timebarAttrs, R.styleable.DefaultTimeBar, 0, 0); try { scrubberDrawable = a.getDrawable(R.styleable.DefaultTimeBar_scrubber_drawable); if (scrubberDrawable != null) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 552774fe47..3a194e091a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -28,6 +28,7 @@ import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; @@ -97,6 +98,9 @@ import java.util.Locale; *

  • Corresponding method: None *
  • Default: {@code R.layout.exo_player_control_view} * + *
  • All attributes that can be set on {@link DefaultTimeBar} can also be set on a + * PlayerControlView, and will be propagated to the inflated {@link DefaultTimeBar} unless the + * layout is overridden to specify a custom {@code exo_progress} (see below). * * *

    Overriding the layout file

    @@ -154,7 +158,15 @@ import java.util.Locale; *
      *
    • Type: {@link TextView} *
    + *
  • {@code exo_progress_placeholder} - A placeholder that's replaced with the inflated + * {@link DefaultTimeBar}. Ignored if an {@code exo_progress} view exists. + *
      + *
    • Type: {@link View} + *
    *
  • {@code exo_progress} - Time bar that's updated during playback and allows seeking. + * {@link DefaultTimeBar} attributes set on the PlayerControlView will not be automatically + * propagated through to this instance. If a view exists with this id, any {@code + * exo_progress_placeholder} view will be ignored. *
      *
    • Type: {@link TimeBar} *
    @@ -188,6 +200,18 @@ public class PlayerControlView extends FrameLayout { void onVisibilityChange(int visibility); } + /** Listener to be notified when progress has been updated. */ + public interface ProgressUpdateListener { + + /** + * Called when progress needs to be updated. + * + * @param position The current position. + * @param bufferedPosition The current buffered position. + */ + void onProgressUpdate(long position, long bufferedPosition); + } + /** The default fast forward increment, in milliseconds. */ public static final int DEFAULT_FAST_FORWARD_MS = 15000; /** The default rewind increment, in milliseconds. */ @@ -235,7 +259,8 @@ public class PlayerControlView extends FrameLayout { @Nullable private Player player; private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; - private VisibilityListener visibilityListener; + @Nullable private VisibilityListener visibilityListener; + @Nullable private ProgressUpdateListener progressUpdateListener; @Nullable private PlaybackPreparer playbackPreparer; private boolean isAttachedToWindow; @@ -256,19 +281,22 @@ public class PlayerControlView extends FrameLayout { private long currentWindowOffset; public PlayerControlView(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public PlayerControlView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + public PlayerControlView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, /* defStyleAttr= */ 0); } - public PlayerControlView(Context context, AttributeSet attrs, int defStyleAttr) { + public PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, attrs); } public PlayerControlView( - Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { + Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + @Nullable AttributeSet playbackAttrs) { super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_player_control_view; rewindMs = DEFAULT_REWIND_MS; @@ -317,9 +345,27 @@ public class PlayerControlView extends FrameLayout { LayoutInflater.from(context).inflate(controllerLayoutId, this); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + TimeBar customTimeBar = findViewById(R.id.exo_progress); + View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder); + if (customTimeBar != null) { + timeBar = customTimeBar; + } else if (timeBarPlaceholder != null) { + // Propagate attrs as timebarAttrs so that DefaultTimeBar's custom attributes are transferred, + // but standard attributes (e.g. background) are not. + DefaultTimeBar defaultTimeBar = new DefaultTimeBar(context, null, 0, playbackAttrs); + defaultTimeBar.setId(R.id.exo_progress); + defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams()); + ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent()); + int timeBarIndex = parent.indexOfChild(timeBarPlaceholder); + parent.removeView(timeBarPlaceholder); + parent.addView(defaultTimeBar, timeBarIndex); + timeBar = defaultTimeBar; + } else { + timeBar = null; + } durationView = findViewById(R.id.exo_duration); positionView = findViewById(R.id.exo_position); - timeBar = findViewById(R.id.exo_progress); + if (timeBar != null) { timeBar.addListener(componentListener); } @@ -448,16 +494,27 @@ public class PlayerControlView extends FrameLayout { /** * Sets the {@link VisibilityListener}. * - * @param listener The listener to be notified about visibility changes. + * @param listener The listener to be notified about visibility changes, or null to remove the + * current listener. */ - public void setVisibilityListener(VisibilityListener listener) { + public void setVisibilityListener(@Nullable VisibilityListener listener) { this.visibilityListener = listener; } + /** + * Sets the {@link ProgressUpdateListener}. + * + * @param listener The listener to be notified about when progress is updated. + */ + public void setProgressUpdateListener(@Nullable ProgressUpdateListener listener) { + this.progressUpdateListener = listener; + } + /** * Sets the {@link PlaybackPreparer}. * - * @param playbackPreparer The {@link PlaybackPreparer}. + * @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback + * preparer. */ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) { this.playbackPreparer = playbackPreparer; @@ -855,6 +912,9 @@ public class PlayerControlView extends FrameLayout { timeBar.setPosition(position); timeBar.setBufferedPosition(bufferedPosition); } + if (progressUpdateListener != null) { + progressUpdateListener.onProgressUpdate(position, bufferedPosition); + } // Cancel any pending updates and schedule a new one if necessary. removeCallbacks(updateProgressAction); @@ -1154,8 +1214,7 @@ public class PlayerControlView extends FrameLayout { } @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { updateNavigation(); updateTimeline(); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index aa9e4b1492..c69cb21704 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -385,6 +385,26 @@ public class PlayerNotificationManager { private boolean wasPlayWhenReady; private int lastPlaybackState; + /** + * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int, + * MediaDescriptionAdapter)}. + */ + @Deprecated + public static PlayerNotificationManager createWithNotificationChannel( + Context context, + String channelId, + @StringRes int channelName, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter) { + return createWithNotificationChannel( + context, + channelId, + channelName, + /* channelDescription= */ 0, + notificationId, + mediaDescriptionAdapter); + } + /** * Creates a notification manager and a low-priority notification channel with the specified * {@code channelId} and {@code channelName}. @@ -397,8 +417,12 @@ public class PlayerNotificationManager { * * @param context The {@link Context}. * @param channelId The id of the notification channel. - * @param channelName A string resource identifier for the user visible name of the channel. The - * recommended maximum length is 40 characters; the value may be truncated if it is too long. + * @param channelName A string resource identifier for the user visible name of the notification + * channel. The recommended maximum length is 40 characters. The string may be truncated if + * it's too long. + * @param channelDescription A string resource identifier for the user visible description of the + * notification channel, or 0 if no description is provided. The recommended maximum length is + * 300 characters. The value may be truncated if it is too long. * @param notificationId The id of the notification. * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. */ @@ -406,14 +430,37 @@ public class PlayerNotificationManager { Context context, String channelId, @StringRes int channelName, + @StringRes int channelDescription, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter) { NotificationUtil.createNotificationChannel( - context, channelId, channelName, NotificationUtil.IMPORTANCE_LOW); + context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); return new PlayerNotificationManager( context, channelId, notificationId, mediaDescriptionAdapter); } + /** + * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int, + * MediaDescriptionAdapter, NotificationListener)}. + */ + @Deprecated + public static PlayerNotificationManager createWithNotificationChannel( + Context context, + String channelId, + @StringRes int channelName, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter, + @Nullable NotificationListener notificationListener) { + return createWithNotificationChannel( + context, + channelId, + channelName, + /* channelDescription= */ 0, + notificationId, + mediaDescriptionAdapter, + notificationListener); + } + /** * Creates a notification manager and a low-priority notification channel with the specified * {@code channelId} and {@code channelName}. The {@link NotificationListener} passed as the last @@ -422,7 +469,9 @@ public class PlayerNotificationManager { * @param context The {@link Context}. * @param channelId The id of the notification channel. * @param channelName A string resource identifier for the user visible name of the channel. The - * recommended maximum length is 40 characters; the value may be truncated if it is too long. + * recommended maximum length is 40 characters. The string may be truncated if it's too long. + * @param channelDescription A string resource identifier for the user visible description of the + * channel, or 0 if no description is provided. * @param notificationId The id of the notification. * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. * @param notificationListener The {@link NotificationListener}. @@ -431,11 +480,12 @@ public class PlayerNotificationManager { Context context, String channelId, @StringRes int channelName, + @StringRes int channelDescription, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter, @Nullable NotificationListener notificationListener) { NotificationUtil.createNotificationChannel( - context, channelId, channelName, NotificationUtil.IMPORTANCE_LOW); + context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); return new PlayerNotificationManager( context, channelId, notificationId, mediaDescriptionAdapter, notificationListener); } @@ -966,7 +1016,8 @@ public class PlayerNotificationManager { @Nullable NotificationCompat.Builder builder, boolean ongoing, @Nullable Bitmap largeIcon) { - if (player.getPlaybackState() == Player.STATE_IDLE) { + if (player.getPlaybackState() == Player.STATE_IDLE + && (player.getCurrentTimeline().isEmpty() || playbackPreparer == null)) { builderActions = null; return null; } @@ -1285,7 +1336,7 @@ public class PlayerNotificationManager { } @Override - public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { + public void onTimelineChanged(Timeline timeline, int reason) { startOrUpdateNotification(); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index c776898bc6..0d66922cab 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -34,7 +34,6 @@ import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.Surface; import android.view.SurfaceView; import android.view.TextureView; import android.view.View; @@ -49,8 +48,8 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; -import com.google.android.exoplayer2.Player.VideoComponent; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; @@ -164,9 +163,10 @@ import java.util.List; *
  • Corresponding method: None *
  • Default: {@code R.layout.exo_player_control_view} * - *
  • All attributes that can be set on a {@link PlayerControlView} can also be set on a - * PlayerView, and will be propagated to the inflated {@link PlayerControlView} unless the - * layout is overridden to specify a custom {@code exo_controller} (see below). + *
  • All attributes that can be set on {@link PlayerControlView} and {@link DefaultTimeBar} can + * also be set on a PlayerView, and will be propagated to the inflated {@link + * PlayerControlView} unless the layout is overridden to specify a custom {@code + * exo_controller} (see below). * * *

    Overriding the layout file

    @@ -216,9 +216,10 @@ import java.util.List; *
  • Type: {@link View} * *
  • {@code exo_controller} - An already inflated {@link PlayerControlView}. Allows use - * of a custom extension of {@link PlayerControlView}. Note that attributes such as {@code - * rewind_increment} will not be automatically propagated through to this instance. If a view - * exists with this id, any {@code exo_controller_placeholder} view will be ignored. + * of a custom extension of {@link PlayerControlView}. {@link PlayerControlView} and {@link + * DefaultTimeBar} attributes set on the PlayerView will not be automatically propagated + * through to this instance. If a view exists with this id, any {@code + * exo_controller_placeholder} view will be ignored. *
      *
    • Type: {@link PlayerControlView} *
    @@ -303,16 +304,18 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private boolean controllerHideOnTouch; private int textureViewRotation; private boolean isTouching; + private static final int PICTURE_TYPE_FRONT_COVER = 3; + private static final int PICTURE_TYPE_NOT_SET = -1; public PlayerView(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public PlayerView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + public PlayerView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, /* defStyleAttr= */ 0); } - public PlayerView(Context context, AttributeSet attrs, int defStyleAttr) { + public PlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); if (isInEditMode()) { @@ -405,7 +408,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider break; case SURFACE_TYPE_MONO360_VIEW: SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context); - sphericalSurfaceView.setSurfaceListener(componentListener); sphericalSurfaceView.setSingleTapListener(componentListener); surfaceView = sphericalSurfaceView; break; @@ -459,8 +461,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider this.controller = customController; } else if (controllerPlaceholder != null) { // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are - // transferred, but standard FrameLayout attributes (e.g. background) are not. + // transferred, but standard attributes (e.g. background) are not. this.controller = new PlayerControlView(context, null, 0, attrs); + controller.setId(R.id.exo_controller); controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); int controllerIndex = parent.indexOfChild(controllerPlaceholder); @@ -502,6 +505,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } /** Returns the player currently set on this view, or null if no player is set. */ + @Nullable public Player getPlayer() { return player; } @@ -621,7 +625,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } /** Returns the default artwork to display. */ - public @Nullable Drawable getDefaultArtwork() { + @Nullable + public Drawable getDefaultArtwork() { return defaultArtwork; } @@ -771,11 +776,20 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider if (player != null && player.isPlayingAd()) { return super.dispatchKeyEvent(event); } - boolean isDpadWhenControlHidden = - isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); - boolean handled = - isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - if (handled) { + + boolean isDpadAndUseController = isDpadKey(event.getKeyCode()) && useController; + boolean handled = false; + if (isDpadAndUseController && !controller.isVisible()) { + // Handle the key event by showing the controller. + maybeShowController(true); + handled = true; + } else if (dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event)) { + // The key event was handled as a media key or by the super class. We should also show the + // controller, or extend its show timeout if already visible. + maybeShowController(true); + handled = true; + } else if (isDpadAndUseController) { + // The key event wasn't handled, but we should extend the controller's show timeout. maybeShowController(true); } return handled; @@ -891,9 +905,11 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider /** * Set the {@link PlayerControlView.VisibilityListener}. * - * @param listener The listener to be notified about visibility changes. + * @param listener The listener to be notified about visibility changes, or null to remove the + * current listener. */ - public void setControllerVisibilityListener(PlayerControlView.VisibilityListener listener) { + public void setControllerVisibilityListener( + @Nullable PlayerControlView.VisibilityListener listener) { Assertions.checkState(controller != null); controller.setVisibilityListener(listener); } @@ -901,7 +917,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider /** * Sets the {@link PlaybackPreparer}. * - * @param playbackPreparer The {@link PlaybackPreparer}. + * @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback + * preparer. */ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) { Assertions.checkState(controller != null); @@ -993,7 +1010,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @param listener The listener to be notified about aspect ratios changes of the video content or * the content frame. */ - public void setAspectRatioListener(AspectRatioFrameLayout.AspectRatioListener listener) { + public void setAspectRatioListener( + @Nullable AspectRatioFrameLayout.AspectRatioListener listener) { Assertions.checkState(contentFrame != null); contentFrame.setAspectRatioListener(listener); } @@ -1012,6 +1030,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @return The {@link SurfaceView}, {@link TextureView}, {@link SphericalSurfaceView} or {@code * null}. */ + @Nullable public View getVideoSurfaceView() { return surfaceView; } @@ -1034,12 +1053,16 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the * subtitle view is not present. */ + @Nullable public SubtitleView getSubtitleView() { return subtitleView; } @Override public boolean onTouchEvent(MotionEvent event) { + if (!useController || player == null) { + return false; + } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isTouching = true; @@ -1236,15 +1259,32 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } private boolean setArtworkFromMetadata(Metadata metadata) { + boolean isArtworkSet = false; + int currentPictureType = PICTURE_TYPE_NOT_SET; for (int i = 0; i < metadata.length(); i++) { Metadata.Entry metadataEntry = metadata.get(i); + int pictureType; + byte[] bitmapData; if (metadataEntry instanceof ApicFrame) { - byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; + bitmapData = ((ApicFrame) metadataEntry).pictureData; + pictureType = ((ApicFrame) metadataEntry).pictureType; + } else if (metadataEntry instanceof PictureFrame) { + bitmapData = ((PictureFrame) metadataEntry).pictureData; + pictureType = ((PictureFrame) metadataEntry).pictureType; + } else { + continue; + } + // Prefer the first front cover picture. If there aren't any, prefer the first picture. + if (currentPictureType == PICTURE_TYPE_NOT_SET || pictureType == PICTURE_TYPE_FRONT_COVER) { Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); - return setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); + isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); + currentPictureType = pictureType; + if (currentPictureType == PICTURE_TYPE_FRONT_COVER) { + break; + } } } - return false; + return isArtworkSet; } private boolean setDrawableArtwork(@Nullable Drawable drawable) { @@ -1368,7 +1408,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider TextOutput, VideoListener, OnLayoutChangeListener, - SphericalSurfaceView.SurfaceListener, SingleTapListener { // TextOutput implementation @@ -1458,18 +1497,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider applyTextureViewRotation((TextureView) view, textureViewRotation); } - // SphericalSurfaceView.SurfaceTextureListener implementation - - @Override - public void surfaceChanged(@Nullable Surface surface) { - if (player != null) { - VideoComponent videoComponent = player.getVideoComponent(); - if (videoComponent != null) { - videoComponent.setVideoSurface(surface); - } - } - } - // SingleTapListener implementation @Override diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 4f22362de6..9ed1bbd006 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -362,10 +362,16 @@ import com.google.android.exoplayer2.util.Util; int width = Math.round(parentWidth * cueSize); int height = cueBitmapHeight != Cue.DIMEN_UNSET ? Math.round(parentHeight * cueBitmapHeight) : Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); - int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) - : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); - int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) - : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); + int x = + Math.round( + cuePositionAnchor == Cue.ANCHOR_TYPE_END + ? (anchorX - width) + : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); + int y = + Math.round( + cueLineAnchor == Cue.ANCHOR_TYPE_END + ? (anchorY - height) + : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); bitmapRect = new Rect(x, y, x + width, y + height); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 5d99eda109..0bdc1acc88 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -53,8 +53,8 @@ public final class SubtitleView extends View implements TextOutput { private final List painters; - private List cues; - private @Cue.TextSizeType int textSizeType; + @Nullable private List cues; + @Cue.TextSizeType private int textSizeType; private float textSize; private boolean applyEmbeddedStyles; private boolean applyEmbeddedFontSizes; @@ -62,10 +62,10 @@ public final class SubtitleView extends View implements TextOutput { private float bottomPaddingFraction; public SubtitleView(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public SubtitleView(Context context, AttributeSet attrs) { + public SubtitleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); painters = new ArrayList<>(); textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; @@ -246,7 +246,11 @@ public final class SubtitleView extends View implements TextOutput { @Override public void dispatchDraw(Canvas canvas) { - int cueCount = (cues == null) ? 0 : cues.size(); + List cues = this.cues; + if (cues == null || cues.isEmpty()) { + return; + } + int rawViewHeight = getHeight(); // Calculate the cue box bounds relative to the canvas after padding is taken into account. @@ -267,6 +271,7 @@ public final class SubtitleView extends View implements TextOutput { return; } + int cueCount = cues.size(); for (int i = 0; i < cueCount; i++) { Cue cue = cues.get(i); float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java index c55cf31149..02ed0a534e 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java @@ -67,7 +67,7 @@ public class TrackSelectionView extends LinearLayout { private TrackNameProvider trackNameProvider; private CheckedTextView[][] trackViews; - @MonotonicNonNull private MappedTrackInfo mappedTrackInfo; + private @MonotonicNonNull MappedTrackInfo mappedTrackInfo; private int rendererIndex; private TrackGroupArray trackGroups; private boolean isDisabled; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/package-info.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/package-info.java new file mode 100644 index 0000000000..85903f4659 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/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.ui; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java index 3d7e57bbd2..6ef9d4907d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java @@ -101,8 +101,8 @@ public final class CanvasRenderer { // GL initialization. The client of this class acquires a Canvas from the Surface, writes to it // and posts it. This marks the Surface as dirty. The GL code then updates the SurfaceTexture // when rendering only if it is dirty. - @MonotonicNonNull private SurfaceTexture displaySurfaceTexture; - @MonotonicNonNull private Surface displaySurface; + private @MonotonicNonNull SurfaceTexture displaySurfaceTexture; + private @MonotonicNonNull Surface displaySurface; public CanvasRenderer() { vertexBuffer = GlUtil.createBuffer(COORDS_PER_VERTEX * VERTEX_COUNT); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java index f24bcce3ce..9a8c787e77 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java @@ -19,11 +19,11 @@ import static com.google.android.exoplayer2.util.GlUtil.checkGlError; import android.opengl.GLES11Ext; import android.opengl.GLES20; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.GlUtil; import com.google.android.exoplayer2.video.spherical.Projection; import java.nio.FloatBuffer; -import org.checkerframework.checker.nullness.qual.Nullable; /** * Utility class to render spherical meshes for video or images. Call {@link #init()} on the GL @@ -91,8 +91,8 @@ import org.checkerframework.checker.nullness.qual.Nullable; }; private int stereoMode; - private @Nullable MeshData leftMeshData; - private @Nullable MeshData rightMeshData; + @Nullable private MeshData leftMeshData; + @Nullable private MeshData rightMeshData; // Program related GL items. These are only valid if program != 0. private int program; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java index 2889351f19..b70fd277a9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java @@ -54,7 +54,7 @@ public final class SceneRenderer implements VideoFrameMetadataListener, CameraMo // Used by other threads only private volatile @C.StreamType int defaultStereoMode; private @C.StreamType int lastStereoMode; - private @Nullable byte[] lastProjectionData; + @Nullable private byte[] lastProjectionData; // Methods called on any thread. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java index f7b208d085..67bc992558 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java @@ -53,20 +53,6 @@ import javax.microedition.khronos.opengles.GL10; */ public final class SphericalSurfaceView extends GLSurfaceView { - /** - * This listener can be used to be notified when the {@link Surface} associated with this view is - * changed. - */ - public interface SurfaceListener { - /** - * Invoked when the surface is changed or there isn't one anymore. Any previous surface - * shouldn't be used after this call. - * - * @param surface The new surface or null if there isn't one anymore. - */ - void surfaceChanged(@Nullable Surface surface); - } - // Arbitrary vertical field of view. private static final int FIELD_OF_VIEW_DEGREES = 90; private static final float Z_NEAR = .1f; @@ -78,15 +64,14 @@ public final class SphericalSurfaceView extends GLSurfaceView { /* package */ static final float UPRIGHT_ROLL = (float) Math.PI; private final SensorManager sensorManager; - private final @Nullable Sensor orientationSensor; + @Nullable private final Sensor orientationSensor; private final OrientationListener orientationListener; private final Handler mainHandler; private final TouchTracker touchTracker; private final SceneRenderer scene; - private @Nullable SurfaceListener surfaceListener; - private @Nullable SurfaceTexture surfaceTexture; - private @Nullable Surface surface; - private @Nullable Player.VideoComponent videoComponent; + @Nullable private SurfaceTexture surfaceTexture; + @Nullable private Surface surface; + @Nullable private Player.VideoComponent videoComponent; public SphericalSurfaceView(Context context) { this(context, null); @@ -155,15 +140,6 @@ public final class SphericalSurfaceView extends GLSurfaceView { } } - /** - * Sets the {@link SurfaceListener} used to listen to surface events. - * - * @param listener The listener for surface events. - */ - public void setSurfaceListener(@Nullable SurfaceListener listener) { - surfaceListener = listener; - } - /** Sets the {@link SingleTapListener} used to listen to single tap events on this view. */ public void setSingleTapListener(@Nullable SingleTapListener listener) { touchTracker.setSingleTapListener(listener); @@ -195,8 +171,8 @@ public final class SphericalSurfaceView extends GLSurfaceView { mainHandler.post( () -> { if (surface != null) { - if (surfaceListener != null) { - surfaceListener.surfaceChanged(null); + if (videoComponent != null) { + videoComponent.clearVideoSurface(surface); } releaseSurface(surfaceTexture, surface); surfaceTexture = null; @@ -213,8 +189,8 @@ public final class SphericalSurfaceView extends GLSurfaceView { Surface oldSurface = this.surface; this.surfaceTexture = surfaceTexture; this.surface = new Surface(surfaceTexture); - if (surfaceListener != null) { - surfaceListener.surfaceChanged(surface); + if (videoComponent != null) { + videoComponent.setVideoSurface(surface); } releaseSurface(oldSurfaceTexture, oldSurface); }); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java index 142f2fc668..5f3a5275c1 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/TouchTracker.java @@ -65,7 +65,7 @@ import android.view.View; // The conversion from touch to yaw & pitch requires compensating for device roll. This is set // on the sensor thread and read on the UI thread. private volatile float roll; - private @Nullable SingleTapListener singleTapListener; + @Nullable private SingleTapListener singleTapListener; @SuppressWarnings({ "nullness:assignment.type.incompatible", diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/package-info.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/package-info.java new file mode 100644 index 0000000000..bbbffc7a44 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/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.ui.spherical; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/ui/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml index ed2fb8e2b2..027e57ee92 100644 --- a/library/ui/src/main/res/layout/exo_playback_control_view.xml +++ b/library/ui/src/main/res/layout/exo_playback_control_view.xml @@ -76,8 +76,7 @@ android:includeFontPadding="false" android:textColor="#FFBEBEBE"/> - diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index f4a7976ebd..706fba0e0b 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -24,25 +24,43 @@ - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -58,9 +76,11 @@ - + + - + + @@ -69,6 +89,20 @@ + + + + + + + + + + + + + + @@ -83,22 +117,36 @@ + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml index e57301f946..17b55cd731 100644 --- a/library/ui/src/main/res/values/ids.xml +++ b/library/ui/src/main/res/values/ids.xml @@ -33,6 +33,7 @@ + diff --git a/library/ui/src/test/AndroidManifest.xml b/library/ui/src/test/AndroidManifest.xml index 1a749dc82c..b8f7562969 100644 --- a/library/ui/src/test/AndroidManifest.xml +++ b/library/ui/src/test/AndroidManifest.xml @@ -15,4 +15,6 @@ ~ limitations under the License. --> - + + + diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index dd5cfa64a7..5865d3c36d 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -34,7 +34,7 @@ android { dependencies { androidTestImplementation 'androidx.test:rules:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - androidTestImplementation 'androidx.annotation:annotation:1.0.2' + androidTestImplementation 'androidx.annotation:annotation:1.1.0' androidTestImplementation project(modulePrefix + 'library-core') androidTestImplementation project(modulePrefix + 'library-dash') androidTestImplementation project(modulePrefix + 'library-hls') diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index b2a49a31fe..e452e391d5 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -24,7 +24,6 @@ import android.net.Uri; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; @@ -385,8 +384,7 @@ public final class DashTestRunner { MappedTrackInfo mappedTrackInfo, int[][][] rendererFormatSupports, int[] rendererMixedMimeTypeAdaptationSupports, - Parameters parameters) - throws ExoPlaybackException { + Parameters parameters) { Assertions.checkState( mappedTrackInfo.getRendererType(VIDEO_RENDERER_INDEX) == C.TRACK_TYPE_VIDEO); Assertions.checkState( diff --git a/publish.gradle b/publish.gradle index 85cf87aa85..8cfc2b2ea1 100644 --- a/publish.gradle +++ b/publish.gradle @@ -23,6 +23,21 @@ if (project.ext.has("exoplayerPublishEnabled") groupId = 'com.google.android.exoplayer' website = 'https://github.com/google/ExoPlayer' } + + gradle.taskGraph.whenReady { taskGraph -> + project.tasks + .findAll { task -> task.name.contains("generatePomFileFor") } + .forEach { task -> + task.doLast { + task.outputs.files + .filter { File file -> + file.path.contains("publications") \ + && file.name.matches("^pom-.+\\.xml\$") + } + .forEach { File file -> addLicense(file) } + } + } + } } def getBintrayRepo() { @@ -30,3 +45,24 @@ def getBintrayRepo() { property('publicRepo').toBoolean() return publicRepo ? 'exoplayer' : 'exoplayer-test' } + +static void addLicense(File pom) { + def licenseNode = new Node(null, "license") + licenseNode.append( + new Node(null, "name", "The Apache Software License, Version 2.0")) + licenseNode.append( + new Node(null, "url", "http://www.apache.org/licenses/LICENSE-2.0.txt")) + licenseNode.append(new Node(null, "distribution", "repo")) + def licensesNode = new Node(null, "licenses") + licensesNode.append(licenseNode) + + def xml = new XmlParser().parse(pom) + xml.append(licensesNode) + + def writer = new PrintWriter(new FileWriter(pom)) + writer.write("\n") + def printer = new XmlNodePrinter(writer) + printer.preserveWhitespace = true + printer.print(xml) + writer.close() +} diff --git a/settings.gradle b/settings.gradle index d4530d67b7..50fdb68f30 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,10 +21,12 @@ if (gradle.ext.has('exoplayerModulePrefix')) { include modulePrefix + 'demo' include modulePrefix + 'demo-cast' include modulePrefix + 'demo-ima' +include modulePrefix + 'demo-gvr' include modulePrefix + 'playbacktests' project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main') project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast') project(modulePrefix + 'demo-ima').projectDir = new File(rootDir, 'demos/ima') +project(modulePrefix + 'demo-gvr').projectDir = new File(rootDir, 'demos/gvr') project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') apply from: 'core_settings.gradle' diff --git a/testutils/build.gradle b/testutils/build.gradle index 36465f5d5f..b5e68187be 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -39,11 +39,13 @@ android { dependencies { api 'org.mockito:mockito-core:' + mockitoVersion + api 'androidx.test:core:' + androidXTestVersion api 'androidx.test.ext:junit:' + androidXTestVersion api 'com.google.truth:truth:' + truthVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation project(modulePrefix + 'library-core') implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion - testImplementation project(modulePrefix + 'testutils-robolectric') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index facbe8bbde..5d07f986d2 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -43,7 +43,7 @@ import com.google.android.exoplayer2.util.Log; public abstract class Action { private final String tag; - private final @Nullable String description; + @Nullable private final String description; /** * @param tag A tag to use for logging. @@ -542,12 +542,10 @@ public abstract class Action { } } - /** - * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)}. - */ + /** Waits for {@link Player.EventListener#onTimelineChanged(Timeline, int)}. */ public static final class WaitForTimelineChanged extends Action { - private final @Nullable Timeline expectedTimeline; + @Nullable private final Timeline expectedTimeline; /** * Creates action waiting for a timeline change. @@ -575,9 +573,7 @@ public abstract class Action { new Player.EventListener() { @Override public void onTimelineChanged( - Timeline timeline, - @Nullable Object manifest, - @Player.TimelineChangeReason int reason) { + Timeline timeline, @Player.TimelineChangeReason int reason) { if (expectedTimeline == null || timeline.equals(expectedTimeline)) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 7f688cacf7..735156e64c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -613,7 +613,7 @@ public final class ActionSchedule { */ private static final class CallbackAction extends Action { - private @Nullable Callback callback; + @Nullable private Callback callback; public CallbackAction(String tag) { super(tag, "FinishedCallback"); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java similarity index 56% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index 9a17904379..4ea4c0844e 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -33,58 +33,89 @@ import java.util.ArrayList; /** Assertion methods for {@link Cache}. */ public final class CacheAsserts { - /** - * Asserts that the cache content is equal to the data in the {@code fakeDataSet}. - * - * @throws IOException If an error occurred reading from the Cache. - */ - public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { - ArrayList allData = fakeDataSet.getAllData(); - Uri[] uris = new Uri[allData.size()]; - for (int i = 0; i < allData.size(); i++) { - uris[i] = allData.get(i).uri; + /** Defines a set of data requests. */ + public static final class RequestSet { + + private final FakeDataSet fakeDataSet; + private DataSpec[] dataSpecs; + + public RequestSet(FakeDataSet fakeDataSet) { + this.fakeDataSet = fakeDataSet; + ArrayList allData = fakeDataSet.getAllData(); + dataSpecs = new DataSpec[allData.size()]; + for (int i = 0; i < dataSpecs.length; i++) { + dataSpecs[i] = new DataSpec(allData.get(i).uri); + } + } + + public RequestSet subset(String... uriStrings) { + dataSpecs = new DataSpec[uriStrings.length]; + for (int i = 0; i < dataSpecs.length; i++) { + dataSpecs[i] = new DataSpec(Uri.parse(uriStrings[i])); + } + return this; + } + + public RequestSet subset(Uri... uris) { + dataSpecs = new DataSpec[uris.length]; + for (int i = 0; i < dataSpecs.length; i++) { + dataSpecs[i] = new DataSpec(uris[i]); + } + return this; + } + + public RequestSet subset(DataSpec... dataSpecs) { + this.dataSpecs = dataSpecs; + return this; + } + + public int getCount() { + return dataSpecs.length; + } + + public byte[] getData(int i) { + return fakeDataSet.getData(dataSpecs[i].uri).getData(); + } + + public DataSpec getDataSpec(int i) { + return dataSpecs[i]; + } + + public RequestSet useBoundedDataSpecFor(String uriString) { + FakeData data = fakeDataSet.getData(uriString); + for (int i = 0; i < dataSpecs.length; i++) { + DataSpec spec = dataSpecs[i]; + if (spec.uri.getPath().equals(uriString)) { + dataSpecs[i] = spec.subrange(0, data.getData().length); + return this; + } + } + throw new IllegalStateException(); } - assertCachedData(cache, fakeDataSet, uris); } /** - * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + * Asserts that the cache contains necessary data for the {@code requestSet}. * * @throws IOException If an error occurred reading from the Cache. */ - public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) - throws IOException { - Uri[] uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); - } - assertCachedData(cache, fakeDataSet, uris); - } - - /** - * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. - * - * @throws IOException If an error occurred reading from the Cache. - */ - public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris) - throws IOException { + public static void assertCachedData(Cache cache, RequestSet requestSet) throws IOException { int totalLength = 0; - for (Uri uri : uris) { - byte[] data = fakeDataSet.getData(uri).getData(); - assertDataCached(cache, uri, data); + for (int i = 0; i < requestSet.getCount(); i++) { + byte[] data = requestSet.getData(i); + assertDataCached(cache, requestSet.getDataSpec(i), data); totalLength += data.length; } assertThat(cache.getCacheSpace()).isEqualTo(totalLength); } /** - * Asserts that the cache contains the given data for {@code uriString}. + * Asserts that the cache content is equal to the data in the {@code fakeDataSet}. * * @throws IOException If an error occurred reading from the Cache. */ - public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { - DataSpec dataSpec = new DataSpec(uri); - assertDataCached(cache, dataSpec, expected); + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { + assertCachedData(cache, new RequestSet(fakeDataSet)); } /** @@ -95,15 +126,18 @@ public final class CacheAsserts { public static void assertDataCached(Cache cache, DataSpec dataSpec, byte[] expected) throws IOException { DataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); - dataSource.open(dataSpec); + byte[] bytes; try { - byte[] bytes = TestUtil.readToEnd(dataSource); - assertWithMessage("Cached data doesn't match expected for '" + dataSpec.uri + "',") - .that(bytes) - .isEqualTo(expected); + dataSource.open(dataSpec); + bytes = TestUtil.readToEnd(dataSource); + } catch (IOException e) { + throw new IOException("Opening/reading cache failed: " + dataSpec, e); } finally { dataSource.close(); } + assertWithMessage("Cached data doesn't match expected for '" + dataSpec.uri + "',") + .that(bytes) + .isEqualTo(expected); } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 2b479c549a..d6b72048a1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -31,7 +31,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; -import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.nio.ByteBuffer; @@ -56,6 +55,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + boolean enableDecoderFallback, Handler eventHandler, VideoRendererEventListener eventListener, long allowedVideoJoiningTimeMs, @@ -114,8 +114,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { MediaCodec codec, Format format, MediaCrypto crypto, - float operatingRate) - throws DecoderQueryException { + float operatingRate) { // If the codec is being initialized whilst the renderer is started, default behavior is to // render the first frame (i.e. the keyframe before the current position), then drop frames up // to the current playback position. For test runs that place a maximum limit on the number of @@ -165,14 +164,15 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException { if (skipToPositionBeforeRenderingFirstFrame && bufferPresentationTimeUs < positionUs) { // After the codec has been initialized, don't render the first frame until we've caught up // to the playback position. Else test runs on devices that do not support dummy surface // will drop frames between rendering the first one and catching up [Internal: b/66494991]. - shouldSkip = true; + isDecodeOnlyBuffer = true; } return super.processOutputBuffer( positionUs, @@ -182,7 +182,8 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { bufferIndex, bufferFlags, bufferPresentationTimeUs, - shouldSkip, + isDecodeOnlyBuffer, + isLastBuffer, format); } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/DefaultRenderersFactoryAsserts.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 90f2294bfc..3ebd47b7a6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -37,7 +37,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.HostActivity.HostedTest; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.util.Clock; @@ -238,7 +237,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { } protected DefaultTrackSelector buildTrackSelector(HostActivity host) { - return new DefaultTrackSelector(new AdaptiveTrackSelection.Factory()); + return new DefaultTrackSelector(host); } protected SimpleExoPlayer buildExoPlayer( diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 0d55dd8530..7db1987d5b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -284,7 +284,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc supportedFormats = new Format[] {VIDEO_FORMAT}; } if (trackSelector == null) { - trackSelector = new DefaultTrackSelector(); + trackSelector = new DefaultTrackSelector(context); } if (bandwidthMeter == null) { bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build(); @@ -309,9 +309,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc } if (mediaSource == null) { if (timeline == null) { - timeline = new FakeTimeline(1); + timeline = new FakeTimeline(/* windowCount= */ 1, manifest); } - mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats); + mediaSource = new FakeMediaSource(timeline, supportedFormats); } if (expectedPlayerEndedCount == null) { expectedPlayerEndedCount = 1; @@ -338,16 +338,15 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc private final DefaultTrackSelector trackSelector; private final LoadControl loadControl; private final BandwidthMeter bandwidthMeter; - private final @Nullable ActionSchedule actionSchedule; - private final @Nullable Player.EventListener eventListener; - private final @Nullable AnalyticsListener analyticsListener; + @Nullable private final ActionSchedule actionSchedule; + @Nullable private final Player.EventListener eventListener; + @Nullable private final AnalyticsListener analyticsListener; private final HandlerThread playerThread; private final HandlerWrapper handler; private final CountDownLatch endedCountDownLatch; private final CountDownLatch actionScheduleFinishedCountDownLatch; private final ArrayList timelines; - private final ArrayList manifests; private final ArrayList timelineChangeReasons; private final ArrayList periodIndices; private final ArrayList discontinuityReasons; @@ -380,7 +379,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc this.eventListener = eventListener; this.analyticsListener = analyticsListener; this.timelines = new ArrayList<>(); - this.manifests = new ArrayList<>(); this.timelineChangeReasons = new ArrayList<>(); this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); @@ -418,7 +416,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (actionSchedule != null) { actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } - player.prepare(mediaSource); + player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); } catch (Exception e) { handleException(e); } @@ -469,9 +467,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc // Assertions called on the test thread after test finished. /** - * Asserts that the timelines reported by - * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided - * timelines. + * Asserts that the timelines reported by {@link Player.EventListener#onTimelineChanged(Timeline, + * int)} are equal to the provided timelines. * * @param timelines A list of expected {@link Timeline}s. */ @@ -479,21 +476,10 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc assertThat(this.timelines).containsExactlyElementsIn(Arrays.asList(timelines)).inOrder(); } - /** - * Asserts that the manifests reported by - * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided - * manifest. - * - * @param manifests A list of expected manifests. - */ - public void assertManifestsEqual(Object... manifests) { - assertThat(this.manifests).containsExactlyElementsIn(Arrays.asList(manifests)).inOrder(); - } - /** * Asserts that the timeline change reasons reported by {@link - * Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided - * timeline change reasons. + * Player.EventListener#onTimelineChanged(Timeline, int)} are equal to the provided timeline + * change reasons. */ public void assertTimelineChangeReasonsEqual(Integer... reasons) { assertThat(timelineChangeReasons).containsExactlyElementsIn(Arrays.asList(reasons)).inOrder(); @@ -573,10 +559,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc // Player.EventListener @Override - public void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { timelines.add(timeline); - manifests.add(manifest); timelineChangeReasons.add(reason); if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) { periodIndices.add(player.getCurrentPeriodIndex()); @@ -638,7 +622,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc loadControl, /* drmSessionManager= */ null, bandwidthMeter, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(clock), clock, Looper.myLooper()); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java index 3937dabcaf..a933121bc5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java @@ -175,17 +175,26 @@ public final class ExtractorAsserts { extractorOutput.assertOutput(context, file + ".0" + DUMP_EXTENSION); } + // Seeking to (timeUs=0, position=0) should always work, and cause the same data to be output. + extractorOutput.clearTrackOutputs(); + input.reset(); + consumeTestData(extractor, input, /* timeUs= */ 0, extractorOutput, false); + if (simulateUnknownLength && assetExists(context, file + UNKNOWN_LENGTH_EXTENSION)) { + extractorOutput.assertOutput(context, file + UNKNOWN_LENGTH_EXTENSION); + } else { + extractorOutput.assertOutput(context, file + ".0" + DUMP_EXTENSION); + } + + // If the SeekMap is seekable, test seeking to 4 positions in the stream. SeekMap seekMap = extractorOutput.seekMap; if (seekMap.isSeekable()) { long durationUs = seekMap.getDurationUs(); for (int j = 0; j < 4; j++) { + extractorOutput.clearTrackOutputs(); long timeUs = (durationUs * j) / 3; long position = seekMap.getSeekPoints(timeUs).first.position; + input.reset(); input.setPosition((int) position); - for (int i = 0; i < extractorOutput.numberOfTracks; i++) { - extractorOutput.trackOutputs.valueAt(i).clear(); - } - consumeTestData(extractor, input, timeUs, extractorOutput, false); extractorOutput.assertOutput(context, file + '.' + j + DUMP_EXTENSION); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 1e3b3bf82b..bcb97be287 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -40,7 +41,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod private final Allocator allocator; private final FakeChunkSource.Factory chunkSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; private final long durationUs; private Callback callback; @@ -149,6 +150,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod /* callback= */ this, allocator, /* positionUs= */ 0, + /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 3), eventDispatcher); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java index 5a158a3659..0d97b7a20f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -34,10 +34,9 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { public FakeAdaptiveMediaSource( Timeline timeline, - Object manifest, TrackGroupArray trackGroupArray, FakeChunkSource.Factory chunkSourceFactory) { - super(timeline, manifest, trackGroupArray); + super(timeline, trackGroupArray); this.chunkSourceFactory = chunkSourceFactory; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java index 77ae19f083..286ef15b15 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java @@ -79,11 +79,11 @@ public class FakeDataSet { */ public static final class Segment { - public @Nullable final IOException exception; - public @Nullable final byte[] data; + @Nullable public final IOException exception; + @Nullable public final byte[] data; public final int length; public final long byteOffset; - public @Nullable final Runnable action; + @Nullable public final Runnable action; public boolean exceptionThrown; public boolean exceptionCleared; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java index c467bd36af..443ffdb12c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java @@ -80,13 +80,23 @@ public final class FakeExtractorInput implements ExtractorInput { failedPeekPositions = new SparseBooleanArray(); } + /** Resets the input to its initial state. */ + public void reset() { + readPosition = 0; + peekPosition = 0; + partiallySatisfiedTargetPositions.clear(); + failedReadPositions.clear(); + failedPeekPositions.clear(); + } + /** * Sets the read and peek positions. * * @param position The position to set. */ public void setPosition(int position) { - assertThat(0 <= position && position <= data.length).isTrue(); + assertThat(0 <= position).isTrue(); + assertThat(position <= data.length).isTrue(); readPosition = position; peekPosition = position; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index c6543bd7a5..4022a0ccc1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -70,6 +70,12 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab this.seekMap = seekMap; } + public void clearTrackOutputs() { + for (int i = 0; i < numberOfTracks; i++) { + trackOutputs.valueAt(i).clear(); + } + } + public void assertEquals(FakeExtractorOutput expected) { assertThat(numberOfTracks).isEqualTo(expected.numberOfTracks); assertThat(tracksEnded).isEqualTo(expected.tracksEnded); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunk.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaChunkIterator.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index b89acae6c8..8e5ba230ac 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -56,11 +56,10 @@ public class FakeMediaSource extends BaseMediaSource { private final ArrayList createdMediaPeriods; protected Timeline timeline; - private Object manifest; private boolean preparedSource; private boolean releasedSource; private Handler sourceInfoRefreshHandler; - private @Nullable TransferListener transferListener; + @Nullable private TransferListener transferListener; /** * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with a @@ -68,8 +67,8 @@ public class FakeMediaSource extends BaseMediaSource { * null to prevent an immediate source info refresh message when preparing the media source. It * can be manually set later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(@Nullable Timeline timeline, Object manifest, Format... formats) { - this(timeline, manifest, buildTrackGroupArray(formats)); + public FakeMediaSource(@Nullable Timeline timeline, Format... formats) { + this(timeline, buildTrackGroupArray(formats)); } /** @@ -78,10 +77,8 @@ public class FakeMediaSource extends BaseMediaSource { * immediate source info refresh message when preparing the media source. It can be manually set * later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(@Nullable Timeline timeline, Object manifest, - TrackGroupArray trackGroupArray) { + public FakeMediaSource(@Nullable Timeline timeline, TrackGroupArray trackGroupArray) { this.timeline = timeline; - this.manifest = manifest; this.activeMediaPeriods = new ArrayList<>(); this.createdMediaPeriods = new ArrayList<>(); this.trackGroupArray = trackGroupArray; @@ -137,7 +134,7 @@ public class FakeMediaSource extends BaseMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { assertThat(preparedSource).isTrue(); assertThat(releasedSource).isFalse(); assertThat(activeMediaPeriods.isEmpty()).isTrue(); @@ -158,12 +155,10 @@ public class FakeMediaSource extends BaseMediaSource { assertThat(releasedSource).isFalse(); assertThat(preparedSource).isTrue(); timeline = newTimeline; - manifest = newManifest; finishSourcePreparation(); }); } else { timeline = newTimeline; - manifest = newManifest; } } @@ -212,7 +207,7 @@ public class FakeMediaSource extends BaseMediaSource { } private void finishSourcePreparation() { - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); if (!timeline.isEmpty()) { MediaLoadData mediaLoadData = new MediaLoadData( diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index a60c1c9c6d..02d0e372e8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -31,7 +31,7 @@ import java.io.IOException; public final class FakeSampleStream implements SampleStream { private final Format format; - private final @Nullable EventDispatcher eventDispatcher; + @Nullable private final EventDispatcher eventDispatcher; private boolean notifiedDownstreamFormat; private boolean readFormat; diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 56438a51ef..58ee32cdd9 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -112,6 +112,7 @@ public final class FakeTimeline extends Timeline { private static final long AD_DURATION_US = 10 * C.MICROS_PER_SECOND; private final TimelineWindowDefinition[] windowDefinitions; + private final Object[] manifests; private final int[] periodOffsets; /** @@ -140,9 +141,10 @@ public final class FakeTimeline extends Timeline { * with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each. * * @param windowCount The number of windows. + * @param manifests The manifests of the windows. */ - public FakeTimeline(int windowCount) { - this(createDefaultWindowDefinitions(windowCount)); + public FakeTimeline(int windowCount, Object... manifests) { + this(manifests, createDefaultWindowDefinitions(windowCount)); } /** @@ -151,6 +153,18 @@ public final class FakeTimeline extends Timeline { * @param windowDefinitions A list of {@link TimelineWindowDefinition}s. */ public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { + this(new Object[0], windowDefinitions); + } + + /** + * Creates a fake timeline with the given window definitions. + * + * @param windowDefinitions A list of {@link TimelineWindowDefinition}s. + */ + public FakeTimeline(Object[] manifests, TimelineWindowDefinition... windowDefinitions) { + this.manifests = new Object[windowDefinitions.length]; + System.arraycopy( + manifests, 0, this.manifests, 0, Math.min(this.manifests.length, manifests.length)); this.windowDefinitions = windowDefinitions; periodOffsets = new int[windowDefinitions.length + 1]; periodOffsets[0] = 0; @@ -171,6 +185,7 @@ public final class FakeTimeline extends Timeline { Object tag = setTag ? windowDefinition.id : null; return window.set( tag, + manifests[windowIndex], /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, windowDefinition.isSeekable, diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 73e8ac4f3e..39429a8fa1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -166,7 +166,8 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(getResources().getIdentifier("host_activity", "layout", getPackageName())); + setContentView( + getResources().getIdentifier("exo_testutils_host_activity", "layout", getPackageName())); surfaceView = findViewById( getResources().getIdentifier("surface_view", "id", getPackageName())); surfaceView.getHolder().addCallback(this); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java similarity index 98% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 0873dbd145..211e85d30c 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.Timeline; 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.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; @@ -199,10 +200,7 @@ public class MediaSourceTestRunner { runOnPlaybackThread(() -> mediaSource.releasePeriod(mediaPeriod)); } - /** - * Calls {@link MediaSource#releaseSource(MediaSource.SourceInfoRefreshListener)} on the playback - * thread. - */ + /** Calls {@link MediaSource#releaseSource(MediaSourceCaller)} on the playback thread. */ public void releaseSource() { runOnPlaybackThread(() -> mediaSource.releaseSource(mediaSourceListener)); } @@ -339,13 +337,12 @@ public class MediaSourceTestRunner { playbackThread.quit(); } - private class MediaSourceListener - implements MediaSource.SourceInfoRefreshListener, MediaSourceEventListener { + private class MediaSourceListener implements MediaSourceCaller, MediaSourceEventListener { - // SourceInfoRefreshListener methods. + // MediaSourceCaller methods. @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); timelines.addLast(timeline); } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java similarity index 93% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 56de0a8b33..eaebe5a12d 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -175,20 +175,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void sendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void blockingSendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - @Override public int getRendererCount() { throw new UnsupportedOperationException(); @@ -209,11 +195,6 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - public Object getCurrentManifest() { - throw new UnsupportedOperationException(); - } - @Override public Timeline getCurrentTimeline() { throw new UnsupportedOperationException(); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java similarity index 100% rename from testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java diff --git a/testutils/src/main/res/layout/host_activity.xml b/testutils/src/main/res/layout/exo_testutils_host_activity.xml similarity index 100% rename from testutils/src/main/res/layout/host_activity.xml rename to testutils/src/main/res/layout/exo_testutils_host_activity.xml diff --git a/testutils/src/test/AndroidManifest.xml b/testutils/src/test/AndroidManifest.xml index e30ea1c3ca..edb8bcafde 100644 --- a/testutils/src/test/AndroidManifest.xml +++ b/testutils/src/test/AndroidManifest.xml @@ -14,4 +14,6 @@ limitations under the License. --> - + + + diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle deleted file mode 100644 index 78fa5dbd87..0000000000 --- a/testutils_robolectric/build.gradle +++ /dev/null @@ -1,46 +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. -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 - } - - lintOptions { - // Robolectric depends on BouncyCastle, which depends on javax.naming, - // which is not part of Android. - disable 'InvalidPackage' - } - - testOptions.unitTests.includeAndroidResources = true -} - -dependencies { - api 'androidx.test:core:' + androidXTestVersion - api 'org.robolectric:robolectric:' + robolectricVersion - api project(modulePrefix + 'testutils') - implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' - annotationProcessor 'com.google.auto.service:auto-service:' + autoServiceVersion -}